diff --git a/.gitignore b/.gitignore index 6b9ae06..feb3352 100644 --- a/.gitignore +++ b/.gitignore @@ -398,3 +398,5 @@ FodyWeavers.xsd *.sln.iml .containers/ + +.vscode/settings.json \ No newline at end of file diff --git a/src/Application/Common/Behaviors/DomainEventBehavior.cs b/src/Application/Common/Behaviors/DomainEventBehavior.cs index b06a551..cc02403 100644 --- a/src/Application/Common/Behaviors/DomainEventBehavior.cs +++ b/src/Application/Common/Behaviors/DomainEventBehavior.cs @@ -20,7 +20,11 @@ public async Task Handle(TRequest request, RequestHandlerDelegate (generic command interface) + var isCommand = request.GetType().GetInterfaces() + .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommand<>)); + + if (isCommand) { logger.LogDebug("Command completed, checking for domain events to dispatch"); diff --git a/src/Application/Features/Auth/Login/LoginCommandHandler.cs b/src/Application/Features/Auth/Login/LoginCommandHandler.cs index 9674c77..c8be5b7 100644 --- a/src/Application/Features/Auth/Login/LoginCommandHandler.cs +++ b/src/Application/Features/Auth/Login/LoginCommandHandler.cs @@ -10,7 +10,7 @@ namespace Application.Features.Auth.Login; /// public sealed class LoginCommandHandler( IUserRepository userRepository, - IPasswordHasher passwordHasher, + IAuthService authService, ITokenService tokenService) : ICommandHandler> { public async Task> Handle(LoginCommand request, CancellationToken cancellationToken) @@ -19,18 +19,19 @@ public async Task> Handle(LoginCommand request, Cancellati var user = await userRepository.GetByEmailAsync(request.Email, cancellationToken) ?? throw new UnauthorizedException("Invalid email or password."); - // Verify password - if (!passwordHasher.VerifyPassword(request.Password, user.PasswordHash)) - { - throw new UnauthorizedException("Invalid email or password."); - } - // Check if user is active if (user.IsDeleted) { throw new ForbiddenException("User account has been deactivated."); } + // Verify password using Identity + var isValidPassword = await authService.CheckPasswordAsync(request.Email, request.Password); + if (!isValidPassword) + { + throw new UnauthorizedException("Invalid email or password."); + } + // Generate tokens var userInfo = new UserInfo { @@ -47,7 +48,7 @@ public async Task> Handle(LoginCommand request, Cancellati { AccessToken = accessToken, RefreshToken = refreshToken, - ExpiresAt = DateTime.UtcNow.AddHours(1), // TODO: Get from configuration + ExpiresAt = DateTime.UtcNow.AddHours(1), User = userInfo }; diff --git a/src/Application/Features/Auth/Register/RegisterCommand.cs b/src/Application/Features/Auth/Register/RegisterCommand.cs new file mode 100644 index 0000000..efeab0a --- /dev/null +++ b/src/Application/Features/Auth/Register/RegisterCommand.cs @@ -0,0 +1,30 @@ +using Application.Common; +using Domain.Common; + +namespace Application.Features.Auth.Register; + +/// +/// Command to register a new user +/// +public sealed record RegisterCommand : ICommand> +{ + /// + /// User's email address + /// + public required string Email { get; init; } + + /// + /// User's password + /// + public required string Password { get; init; } + + /// + /// Confirm password + /// + public required string ConfirmPassword { get; init; } + + /// + /// User's full name + /// + public required string FullName { get; init; } +} diff --git a/src/Application/Features/Auth/Register/RegisterCommandHandler.cs b/src/Application/Features/Auth/Register/RegisterCommandHandler.cs new file mode 100644 index 0000000..f6404c6 --- /dev/null +++ b/src/Application/Features/Auth/Register/RegisterCommandHandler.cs @@ -0,0 +1,79 @@ +using Application.Common; +using Application.Common.Exceptions; +using Application.Features.Auth.Login; +using Application.Interfaces; +using Domain.Common; +using Domain.Constants; +using DomainUser = Domain.Entities.User; + +namespace Application.Features.Auth.Register; + +/// +/// Handler for user registration +/// +public sealed class RegisterCommandHandler( + IUserRepository userRepository, + IAuthService authService, + ITokenService tokenService) : ICommandHandler> +{ + public async Task> Handle(RegisterCommand request, CancellationToken cancellationToken) + { + // Check if email already exists + var existingUser = await userRepository.GetByEmailAsync(request.Email, cancellationToken); + if (existingUser != null) + { + return Result.Failure( + Error.Conflict("User.EmailExists", "Email address is already registered")); + } + + // Create new Domain user + var user = new DomainUser + { + Id = Guid.NewGuid(), + Email = request.Email, + FullName = request.FullName, + Roles = [UserRoles.User], + IsDeleted = false, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + // Save to domain users table + var createdUser = await userRepository.CreateAsync(user, cancellationToken); + + // Create Identity user with same ID and password + var identityCreated = await authService.CreateIdentityUserAsync( + createdUser.Id, + createdUser.Email, + createdUser.FullName, + request.Password, + createdUser.Roles); + + if (!identityCreated) + { + return Result.Failure( + Error.Failure("User.IdentityFailed", "Failed to create Identity user")); + } + + var userInfo = new UserInfo + { + Id = createdUser.Id, + Email = createdUser.Email, + FullName = createdUser.FullName, + Roles = createdUser.Roles + }; + + // Generate tokens for auto-login + var accessToken = tokenService.GenerateAccessToken(userInfo); + var refreshToken = tokenService.GenerateRefreshToken(); + + return Result.Success(new RegisterResponse + { + UserId = createdUser.Id, + Email = createdUser.Email, + FullName = createdUser.FullName, + AccessToken = accessToken, + RefreshToken = refreshToken + }); + } +} diff --git a/src/Application/Features/Auth/Register/RegisterCommandValidator.cs b/src/Application/Features/Auth/Register/RegisterCommandValidator.cs new file mode 100644 index 0000000..da63c6b --- /dev/null +++ b/src/Application/Features/Auth/Register/RegisterCommandValidator.cs @@ -0,0 +1,33 @@ +using FluentValidation; + +namespace Application.Features.Auth.Register; + +/// +/// Validator for RegisterCommand +/// +public sealed class RegisterCommandValidator : AbstractValidator +{ + public RegisterCommandValidator() + { + RuleFor(x => x.Email) + .NotEmpty().WithMessage("Email is required") + .EmailAddress().WithMessage("Invalid email format") + .MaximumLength(200).WithMessage("Email must not exceed 200 characters"); + + RuleFor(x => x.Password) + .NotEmpty().WithMessage("Password is required") + .MinimumLength(8).WithMessage("Password must be at least 8 characters") + .Matches(@"[A-Z]").WithMessage("Password must contain at least one uppercase letter") + .Matches(@"[a-z]").WithMessage("Password must contain at least one lowercase letter") + .Matches(@"[0-9]").WithMessage("Password must contain at least one digit"); + + RuleFor(x => x.ConfirmPassword) + .NotEmpty().WithMessage("Confirm password is required") + .Equal(x => x.Password).WithMessage("Passwords do not match"); + + RuleFor(x => x.FullName) + .NotEmpty().WithMessage("Full name is required") + .MinimumLength(2).WithMessage("Full name must be at least 2 characters") + .MaximumLength(200).WithMessage("Full name must not exceed 200 characters"); + } +} diff --git a/src/Application/Features/Auth/Register/RegisterResponse.cs b/src/Application/Features/Auth/Register/RegisterResponse.cs new file mode 100644 index 0000000..829a19c --- /dev/null +++ b/src/Application/Features/Auth/Register/RegisterResponse.cs @@ -0,0 +1,32 @@ +namespace Application.Features.Auth.Register; + +/// +/// Response after successful registration +/// +public sealed record RegisterResponse +{ + /// + /// User ID + /// + public required Guid UserId { get; init; } + + /// + /// User's email + /// + public required string Email { get; init; } + + /// + /// User's full name + /// + public required string FullName { get; init; } + + /// + /// Access token (optional - auto login after register) + /// + public string? AccessToken { get; init; } + + /// + /// Refresh token (optional - auto login after register) + /// + public string? RefreshToken { get; init; } +} diff --git a/src/Application/Features/User/CreateUser/CreateUserCommand.cs b/src/Application/Features/User/CreateUser/CreateUserCommand.cs deleted file mode 100644 index 1713cac..0000000 --- a/src/Application/Features/User/CreateUser/CreateUserCommand.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Application.Common; -using Application.Common.Behaviors; -using Domain.Common; - -namespace Application.Features.User.CreateUser; - -/// -/// Create user command with transaction and authorization -/// -public sealed record CreateUserCommand : ICommand>, - ITransactionalCommand, IAuthorizedRequest -{ - /// - /// Email address - /// - public required string Email { get; init; } - - /// - /// Full name - /// - public required string FullName { get; init; } - - /// - /// Password - /// - public required string Password { get; init; } - - /// - /// Phone number - /// - public string? PhoneNumber { get; init; } - - /// - /// User roles - /// - public List Roles { get; init; } = ["User"]; - - // IAuthorizedRequest implementation - public AuthorizationRequirement AuthorizationRequirement => new() - { - Roles = ["Admin"], - Permissions = ["users.create"], - RequireAuthentication = true - }; -} - -/// -/// Create user response -/// -public sealed record CreateUserResponse -{ - /// - /// Created user ID - /// - public required Guid Id { get; init; } - - /// - /// Email address - /// - public required string Email { get; init; } - - /// - /// Full name - /// - public required string FullName { get; init; } -} diff --git a/src/Application/Features/User/CreateUser/CreateUserCommandHandler.cs b/src/Application/Features/User/CreateUser/CreateUserCommandHandler.cs deleted file mode 100644 index 15da024..0000000 --- a/src/Application/Features/User/CreateUser/CreateUserCommandHandler.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Application.Common; -using Application.Common.Exceptions; -using Application.Interfaces; -using Domain.Common; -using Domain.Aggregates.User; - -namespace Application.Features.User.CreateUser; - -/// -/// Create user command handler -/// -public sealed class CreateUserCommandHandler( - IUserRepository userRepository, - IPasswordHasher passwordHasher) : ICommandHandler> -{ - public async Task> Handle(CreateUserCommand request, CancellationToken cancellationToken) - { - // Check if user already exists - var existingUser = await userRepository.GetByEmailAsync(request.Email, cancellationToken); - if (existingUser is not null) - { - throw new ConflictException("User", request.Email); - } - - // Create new user - var user = UserAggregate.Create( - request.Email, - request.FullName, - passwordHasher.HashPassword(request.Password), - request.Roles); - - try - { - var createdUser = await userRepository.CreateAsync(user.GetUser(), cancellationToken); - - var response = new CreateUserResponse - { - Id = createdUser.Id, - Email = createdUser.Email, - FullName = createdUser.FullName - }; - - return Result.Success(response); - } - catch (Exception ex) - { - throw new ExternalServiceException("Database", "Failed to create user", ex); - } - } -} diff --git a/src/Application/Features/User/CreateUser/CreateUserCommandValidator.cs b/src/Application/Features/User/CreateUser/CreateUserCommandValidator.cs deleted file mode 100644 index e2aa742..0000000 --- a/src/Application/Features/User/CreateUser/CreateUserCommandValidator.cs +++ /dev/null @@ -1,45 +0,0 @@ -using FluentValidation; - -namespace Application.Features.User.CreateUser; - -/// -/// Create user command validator -/// -public sealed class CreateUserCommandValidator : AbstractValidator -{ - public CreateUserCommandValidator() - { - RuleFor(x => x.Email) - .NotEmpty() - .WithMessage("Email is required") - .EmailAddress() - .WithMessage("Email must be a valid email address") - .MaximumLength(255) - .WithMessage("Email cannot exceed 255 characters"); - - RuleFor(x => x.FullName) - .NotEmpty() - .WithMessage("Full name is required") - .MaximumLength(100) - .WithMessage("Full name cannot exceed 100 characters"); - - RuleFor(x => x.Password) - .NotEmpty() - .WithMessage("Password is required") - .MinimumLength(8) - .WithMessage("Password must be at least 8 characters long") - .Matches(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)") - .WithMessage("Password must contain at least one lowercase, one uppercase letter and one digit"); - - RuleFor(x => x.PhoneNumber) - .Matches(@"^\+?[1-9]\d{1,14}$") - .When(x => !string.IsNullOrEmpty(x.PhoneNumber)) - .WithMessage("Phone number format is invalid"); - - RuleFor(x => x.Roles) - .NotEmpty() - .WithMessage("At least one role is required") - .Must(roles => roles.ToList().TrueForAll(role => !string.IsNullOrWhiteSpace(role))) - .WithMessage("Role names cannot be empty"); - } -} diff --git a/src/Application/Interfaces/IAuthService.cs b/src/Application/Interfaces/IAuthService.cs new file mode 100644 index 0000000..898bb6c --- /dev/null +++ b/src/Application/Interfaces/IAuthService.cs @@ -0,0 +1,32 @@ +namespace Application.Interfaces; + +/// +/// Authentication service interface +/// +public interface IAuthService +{ + /// + /// Create Identity user with password + /// + Task CreateIdentityUserAsync(Guid userId, string email, string fullName, string password, List roles); + + /// + /// Set password for a user + /// + Task SetPasswordAsync(Guid userId, string password); + + /// + /// Check if password is correct + /// + Task CheckPasswordAsync(string email, string password); + + /// + /// Sign in user + /// + Task SignInAsync(string email, string password, bool rememberMe = false); + + /// + /// Sign out user + /// + Task SignOutAsync(); +} diff --git a/src/Domain/Aggregates/User/UserAggregate.cs b/src/Domain/Aggregates/User/UserAggregate.cs deleted file mode 100644 index 50b5361..0000000 --- a/src/Domain/Aggregates/User/UserAggregate.cs +++ /dev/null @@ -1,170 +0,0 @@ -using Domain.Common; -using Domain.Constants; -using Domain.Entities; -using Domain.Events.User; -using Domain.ValueObjects; - -namespace Domain.Aggregates.User; - -/// -/// User aggregate root - Manages user business operations -/// -public sealed class UserAggregate : BaseAggregateRoot -{ - private readonly Entities.User _user; - - public UserAggregate(Entities.User user) - { - _user = user ?? throw new ArgumentNullException(nameof(user)); - Id = user.Id; - } - - // Expose user properties - public string Email => _user.Email; - public string FullName => _user.FullName; - public string PasswordHash => _user.PasswordHash; - public string? PhoneNumber => _user.PhoneNumber; - public List Roles => _user.Roles; - public bool IsActive => _user.IsActive; - - // Get the underlying entity - public Entities.User GetUser() => _user; - - /// - /// Create a new user aggregate - /// - public static UserAggregate Create(string email, string fullName, string passwordHash, List? roles = null) - { - // Validate business rules - if (string.IsNullOrWhiteSpace(email)) - { - throw new ArgumentException("Email is required", nameof(email)); - } - - if (string.IsNullOrWhiteSpace(fullName)) - { - throw new ArgumentException("Full name is required", nameof(fullName)); - } - - if (string.IsNullOrWhiteSpace(passwordHash)) - { - throw new ArgumentException("Password hash is required", nameof(passwordHash)); - } - - // Create user entity - var user = new Entities.User - { - Email = email, - FullName = fullName, - PasswordHash = passwordHash, - Roles = roles?.ToList() ?? [UserRoles.User] - }; - - var aggregate = new UserAggregate(user); - - // Raise domain event - aggregate.AddDomainEvent(new UserCreatedEvent(user.Id, user.Email, user.FullName, user.Roles)); - - return aggregate; - } - - /// - /// Deactivate user (soft delete) - /// - public void Deactivate() - { - // Business rule: Cannot deactivate if user has active conversations - if (HasActiveConversations()) - { - throw new InvalidOperationException("Cannot deactivate user with active conversations"); - } - - _user.IsDeleted = true; - _user.DeletedAt = DateTime.UtcNow; - UpdatedAt = DateTime.UtcNow; - - // Raise domain event - AddDomainEvent(new UserDeactivatedEvent(_user.Id)); - } - - /// - /// Update user information - /// - public void UpdateInfo(string fullName, string? phoneNumber = null) - { - if (string.IsNullOrWhiteSpace(fullName)) - { - throw new ArgumentException("Full name is required", nameof(fullName)); - } - - _user.FullName = fullName; - _user.PhoneNumber = phoneNumber; - _user.UpdatedAt = DateTime.UtcNow; - UpdatedAt = DateTime.UtcNow; - - // Raise domain event - AddDomainEvent(new UserUpdatedEvent(_user.Id, fullName, phoneNumber)); - } - - /// - /// Add role to user - /// - public void AddRole(string role) - { - if (string.IsNullOrWhiteSpace(role)) - { - throw new ArgumentException("Role cannot be empty", nameof(role)); - } - - _user.AddRole(role); - _user.UpdatedAt = DateTime.UtcNow; - UpdatedAt = DateTime.UtcNow; - } - - /// - /// Remove role from user - /// - public void RemoveRole(string role) - { - if (string.IsNullOrWhiteSpace(role)) - { - throw new ArgumentException("Role cannot be empty", nameof(role)); - } - - // Business rule: User must have at least one role - if (_user.Roles.Count <= 1) - { - throw new InvalidOperationException("User must have at least one role"); - } - - _user.RemoveRole(role); - _user.UpdatedAt = DateTime.UtcNow; - UpdatedAt = DateTime.UtcNow; - } - - /// - /// Check if user can perform action - /// - public bool CanPerformAction(string action) - { - // Business logic for permissions - return action switch - { - "CreateConversation" => IsActive, - "EditProfile" => IsActive, - "ManageUsers" => _user.HasRole(UserRoles.Admin), - "AccessLegalDatabase" => _user.HasAnyRole(UserRoles.Admin, UserRoles.LegalExpert), - _ => false - }; - } - - // Private helper methods - private const bool DefaultActiveConversationStatus = false; - - private bool HasActiveConversations() - { - // This would typically be checked via a domain service - // For now, return constant as an example - return DefaultActiveConversationStatus; - } -} diff --git a/src/Domain/Entities/Conversation.cs b/src/Domain/Entities/Conversation.cs index 0a703b5..87bd571 100644 --- a/src/Domain/Entities/Conversation.cs +++ b/src/Domain/Entities/Conversation.cs @@ -9,12 +9,18 @@ namespace Domain.Entities; public sealed class Conversation : BaseEntity { public required Guid OwnerId { get; set; } + public required string Title { get; set; } + public bool IsPrivate { get; set; } + public string[] Tags { get; set; } = []; - public ConversationStatus Status { get; set; } = ConversationStatus.Active; + public bool IsStarred { get; set; } + + public ConversationStatus Status { get; set; } = ConversationStatus.Active; public User Owner { get; set; } = null!; + public ICollection Messages { get; set; } = []; /// @@ -43,13 +49,7 @@ public bool IsAccessibleBy(Guid userId, string[] userRoles) return false; } - /// - /// Check if conversation is active - /// public bool IsActive => Status == ConversationStatus.Active && !IsDeleted; - /// - /// Get message count - /// public int MessageCount => Messages?.Count ?? 0; } diff --git a/src/Domain/Entities/Message.cs b/src/Domain/Entities/Message.cs index 6dd3046..f44a656 100644 --- a/src/Domain/Entities/Message.cs +++ b/src/Domain/Entities/Message.cs @@ -24,7 +24,6 @@ public sealed class Message : BaseEntity public bool IsEdited { get; set; } public DateTime? EditedAt { get; set; } - // Navigation properties public Conversation Conversation { get; set; } = null!; public User Sender { get; set; } = null!; diff --git a/src/Domain/Entities/User.cs b/src/Domain/Entities/User.cs index 041cfc6..f1a61b2 100644 --- a/src/Domain/Entities/User.cs +++ b/src/Domain/Entities/User.cs @@ -3,18 +3,16 @@ namespace Domain.Entities; /// -/// User entity - Pure domain entity +/// Domain User entity for business logic +/// Synchronized with Identity ApplicationUser /// public sealed class User : BaseEntity { public required string Email { get; set; } public required string FullName { get; set; } - public required string PasswordHash { get; set; } public string? PhoneNumber { get; set; } public List Roles { get; set; } = []; - public ICollection OwnedConversations { get; set; } = []; - public ICollection Messages { get; set; } = []; public bool HasRole(string role) => Roles.Contains(role, StringComparer.OrdinalIgnoreCase); diff --git a/src/Infrastructure/Data/Configurations/UserConfiguration.cs b/src/Infrastructure/Data/Configurations/UserConfiguration.cs index 45cb18c..5213967 100644 --- a/src/Infrastructure/Data/Configurations/UserConfiguration.cs +++ b/src/Infrastructure/Data/Configurations/UserConfiguration.cs @@ -26,10 +26,6 @@ public void Configure(EntityTypeBuilder builder) .IsRequired() .HasMaxLength(200); - builder.Property(u => u.PasswordHash) - .IsRequired() - .HasMaxLength(500); - builder.Property(u => u.PhoneNumber) .HasMaxLength(20); diff --git a/src/Infrastructure/Data/Contexts/DataContext.cs b/src/Infrastructure/Data/Contexts/DataContext.cs index db008e5..a688715 100644 --- a/src/Infrastructure/Data/Contexts/DataContext.cs +++ b/src/Infrastructure/Data/Contexts/DataContext.cs @@ -1,30 +1,43 @@ using Domain.Entities; +using Infrastructure.Identity; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace Infrastructure.Data.Contexts; /// -/// Database context cho Legal Assistant application +/// Database context cho Legal Assistant application with Identity integration /// -public sealed class DataContext : DbContext, IDataContext +public sealed class DataContext : IdentityDbContext< + ApplicationUser, + ApplicationRole, + Guid, + IdentityUserClaim, + IdentityUserRole, + IdentityUserLogin, + IdentityRoleClaim, + IdentityUserToken>, IDataContext { public DataContext(DbContextOptions options) : base(options) { } - public DbSet Users => Set(); + // Domain entities in public schema + public DbSet DomainUsers => Set(); public DbSet Conversations => Set(); public DbSet Messages => Set(); - protected override void OnModelCreating(ModelBuilder modelBuilder) + protected override void OnModelCreating(ModelBuilder builder) { - base.OnModelCreating(modelBuilder); + base.OnModelCreating(builder); - // Áp dụng tất cả configurations từ assembly hiện tại - modelBuilder.ApplyConfigurationsFromAssembly(typeof(DataContext).Assembly); + // Configure Identity tables in "identity" schema + ConfigureIdentityTables(builder); - // Cấu hình schema mặc định - modelBuilder.HasDefaultSchema(Schemas.Default); + // Configure application entities in "public" schema + builder.ApplyConfigurationsFromAssembly(typeof(DataContext).Assembly); + builder.HasDefaultSchema(Schemas.Default); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) @@ -35,4 +48,36 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) optionsBuilder.EnableDetailedErrors(); #endif } + + private void ConfigureIdentityTables(ModelBuilder builder) + { + builder.Entity(entity => + { + entity.ToTable("Users", Schemas.Identity); + + entity.Property(u => u.FullName) + .IsRequired() + .HasMaxLength(200); + + entity.Property(u => u.IsDeleted) + .IsRequired() + .HasDefaultValue(false); + + entity.Property(u => u.CreatedAt) + .IsRequired(); + + entity.Property(u => u.UpdatedAt) + .IsRequired(); + + // Query filter for soft delete + entity.HasQueryFilter(u => !u.IsDeleted); + }); + + builder.Entity(entity => entity.ToTable("Roles", Schemas.Identity)); + builder.Entity>(entity => entity.ToTable("UserRoles", Schemas.Identity)); + builder.Entity>(entity => entity.ToTable("UserClaims", Schemas.Identity)); + builder.Entity>(entity => entity.ToTable("UserLogins", Schemas.Identity)); + builder.Entity>(entity => entity.ToTable("RoleClaims", Schemas.Identity)); + builder.Entity>(entity => entity.ToTable("UserTokens", Schemas.Identity)); + } } diff --git a/src/Infrastructure/Data/Contexts/Schemas.cs b/src/Infrastructure/Data/Contexts/Schemas.cs index e15eba3..26504cf 100644 --- a/src/Infrastructure/Data/Contexts/Schemas.cs +++ b/src/Infrastructure/Data/Contexts/Schemas.cs @@ -3,4 +3,5 @@ internal static class Schemas { public const string Default = "public"; + public const string Identity = "identity"; } diff --git a/src/Infrastructure/Data/Migrations/20251027111716_AddIdentityToDataContext.Designer.cs b/src/Infrastructure/Data/Migrations/20251027111716_AddIdentityToDataContext.Designer.cs new file mode 100644 index 0000000..d8095de --- /dev/null +++ b/src/Infrastructure/Data/Migrations/20251027111716_AddIdentityToDataContext.Designer.cs @@ -0,0 +1,534 @@ +// +using System; +using Infrastructure.Data.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20251027111716_AddIdentityToDataContext")] + partial class AddIdentityToDataContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("public") + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Conversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("IsStarred") + .HasColumnType("boolean"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("Conversations", "public"); + }); + + modelBuilder.Entity("Domain.Entities.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EditedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsEdited") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Metadata") + .HasMaxLength(5000) + .HasColumnType("character varying(5000)"); + + b.Property("Role") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("user"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Text"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ConversationId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Role"); + + b.HasIndex("SenderId"); + + b.ToTable("Messages", "public"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IsDeleted"); + + b.ToTable("Users", "public"); + }); + + modelBuilder.Entity("Infrastructure.Identity.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("Roles", "identity"); + }); + + modelBuilder.Entity("Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("Users", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", "identity"); + }); + + modelBuilder.Entity("Domain.Entities.Conversation", b => + { + b.HasOne("Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany("OwnedConversations") + .HasForeignKey("UserId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Domain.Entities.Message", b => + { + b.HasOne("Domain.Entities.Conversation", "Conversation") + .WithMany("Messages") + .HasForeignKey("ConversationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Conversation"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Infrastructure.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Infrastructure.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Conversation", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Navigation("OwnedConversations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Data/Migrations/20251027111716_AddIdentityToDataContext.cs b/src/Infrastructure/Data/Migrations/20251027111716_AddIdentityToDataContext.cs new file mode 100644 index 0000000..620cc80 --- /dev/null +++ b/src/Infrastructure/Data/Migrations/20251027111716_AddIdentityToDataContext.cs @@ -0,0 +1,305 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Data.Migrations; + +/// +public partial class AddIdentityToDataContext : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Messages_Users_UserId", + schema: "public", + table: "Messages"); + + migrationBuilder.DropIndex( + name: "IX_Messages_UserId", + schema: "public", + table: "Messages"); + + migrationBuilder.DropColumn( + name: "UserId", + schema: "public", + table: "Messages"); + + migrationBuilder.EnsureSchema( + name: "identity"); + + migrationBuilder.AddColumn( + name: "IsStarred", + schema: "public", + table: "Conversations", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateTable( + name: "Roles", + schema: "identity", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Description = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true) + }, + constraints: table => table.PrimaryKey("PK_Roles", x => x.Id)); + + migrationBuilder.CreateTable( + name: "Users", + schema: "identity", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + FirstName = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + FullName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + IsDeleted = table.Column(type: "boolean", nullable: false, defaultValue: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) + }, + constraints: table => table.PrimaryKey("PK_Users", x => x.Id)); + + migrationBuilder.CreateTable( + name: "RoleClaims", + schema: "identity", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + RoleId = table.Column(type: "uuid", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_RoleClaims_Roles_RoleId", + column: x => x.RoleId, + principalSchema: "identity", + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserClaims", + schema: "identity", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "uuid", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserClaims", x => x.Id); + table.ForeignKey( + name: "FK_UserClaims_Users_UserId", + column: x => x.UserId, + principalSchema: "identity", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserLogins", + schema: "identity", + columns: table => new + { + LoginProvider = table.Column(type: "text", nullable: false), + ProviderKey = table.Column(type: "text", nullable: false), + ProviderDisplayName = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_UserLogins_Users_UserId", + column: x => x.UserId, + principalSchema: "identity", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserRoles", + schema: "identity", + columns: table => new + { + UserId = table.Column(type: "uuid", nullable: false), + RoleId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_UserRoles_Roles_RoleId", + column: x => x.RoleId, + principalSchema: "identity", + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserRoles_Users_UserId", + column: x => x.UserId, + principalSchema: "identity", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserTokens", + schema: "identity", + columns: table => new + { + UserId = table.Column(type: "uuid", nullable: false), + LoginProvider = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_UserTokens_Users_UserId", + column: x => x.UserId, + principalSchema: "identity", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_RoleClaims_RoleId", + schema: "identity", + table: "RoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + schema: "identity", + table: "Roles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UserClaims_UserId", + schema: "identity", + table: "UserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_UserLogins_UserId", + schema: "identity", + table: "UserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_UserRoles_RoleId", + schema: "identity", + table: "UserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + schema: "identity", + table: "Users", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + schema: "identity", + table: "Users", + column: "NormalizedUserName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RoleClaims", + schema: "identity"); + + migrationBuilder.DropTable( + name: "UserClaims", + schema: "identity"); + + migrationBuilder.DropTable( + name: "UserLogins", + schema: "identity"); + + migrationBuilder.DropTable( + name: "UserRoles", + schema: "identity"); + + migrationBuilder.DropTable( + name: "UserTokens", + schema: "identity"); + + migrationBuilder.DropTable( + name: "Roles", + schema: "identity"); + + migrationBuilder.DropTable( + name: "Users", + schema: "identity"); + + migrationBuilder.DropColumn( + name: "IsStarred", + schema: "public", + table: "Conversations"); + + migrationBuilder.AddColumn( + name: "UserId", + schema: "public", + table: "Messages", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Messages_UserId", + schema: "public", + table: "Messages", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_Messages_Users_UserId", + schema: "public", + table: "Messages", + column: "UserId", + principalSchema: "public", + principalTable: "Users", + principalColumn: "Id"); + } +} diff --git a/src/Infrastructure/Data/Migrations/20251027145835_RemovePasswordHashInDomainUserTable.Designer.cs b/src/Infrastructure/Data/Migrations/20251027145835_RemovePasswordHashInDomainUserTable.Designer.cs new file mode 100644 index 0000000..a0e958f --- /dev/null +++ b/src/Infrastructure/Data/Migrations/20251027145835_RemovePasswordHashInDomainUserTable.Designer.cs @@ -0,0 +1,529 @@ +// +using System; +using Infrastructure.Data.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20251027145835_RemovePasswordHashInDomainUserTable")] + partial class RemovePasswordHashInDomainUserTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("public") + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Conversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("IsStarred") + .HasColumnType("boolean"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("Conversations", "public"); + }); + + modelBuilder.Entity("Domain.Entities.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EditedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsEdited") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Metadata") + .HasMaxLength(5000) + .HasColumnType("character varying(5000)"); + + b.Property("Role") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("user"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Text"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ConversationId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Role"); + + b.HasIndex("SenderId"); + + b.ToTable("Messages", "public"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IsDeleted"); + + b.ToTable("Users", "public"); + }); + + modelBuilder.Entity("Infrastructure.Identity.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("Roles", "identity"); + }); + + modelBuilder.Entity("Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("Users", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", "identity"); + }); + + modelBuilder.Entity("Domain.Entities.Conversation", b => + { + b.HasOne("Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany("OwnedConversations") + .HasForeignKey("UserId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Domain.Entities.Message", b => + { + b.HasOne("Domain.Entities.Conversation", "Conversation") + .WithMany("Messages") + .HasForeignKey("ConversationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Conversation"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Infrastructure.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Infrastructure.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Conversation", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Navigation("OwnedConversations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Data/Migrations/20251027145835_RemovePasswordHashInDomainUserTable.cs b/src/Infrastructure/Data/Migrations/20251027145835_RemovePasswordHashInDomainUserTable.cs new file mode 100644 index 0000000..9371575 --- /dev/null +++ b/src/Infrastructure/Data/Migrations/20251027145835_RemovePasswordHashInDomainUserTable.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Data.Migrations; + +/// +public partial class RemovePasswordHashInDomainUserTable : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PasswordHash", + schema: "public", + table: "Users"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PasswordHash", + schema: "public", + table: "Users", + type: "character varying(500)", + maxLength: 500, + nullable: false, + defaultValue: ""); + } +} diff --git a/src/Infrastructure/Data/Migrations/DataContextModelSnapshot.cs b/src/Infrastructure/Data/Migrations/DataContextModelSnapshot.cs index 9f15d21..0e77018 100644 --- a/src/Infrastructure/Data/Migrations/DataContextModelSnapshot.cs +++ b/src/Infrastructure/Data/Migrations/DataContextModelSnapshot.cs @@ -43,6 +43,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsPrivate") .HasColumnType("boolean"); + b.Property("IsStarred") + .HasColumnType("boolean"); + b.Property("OwnerId") .HasColumnType("uuid"); @@ -133,9 +136,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("timestamp with time zone"); - b.Property("UserId") - .HasColumnType("uuid"); - b.HasKey("Id"); b.HasIndex("ConversationId"); @@ -146,8 +146,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("SenderId"); - b.HasIndex("UserId"); - b.ToTable("Messages", "public"); }); @@ -178,11 +176,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("boolean") .HasDefaultValue(false); - b.Property("PasswordHash") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - b.Property("PhoneNumber") .HasMaxLength(20) .HasColumnType("character varying(20)"); @@ -205,6 +198,234 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Users", "public"); }); + modelBuilder.Entity("Infrastructure.Identity.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("Roles", "identity"); + }); + + modelBuilder.Entity("Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("Users", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", "identity"); + }); + modelBuilder.Entity("Domain.Entities.Conversation", b => { b.HasOne("Domain.Entities.User", "Owner") @@ -234,15 +455,62 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("Domain.Entities.User", null) - .WithMany("Messages") - .HasForeignKey("UserId"); - b.Navigation("Conversation"); b.Navigation("Sender"); }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Infrastructure.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Infrastructure.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Domain.Entities.Conversation", b => { b.Navigation("Messages"); @@ -250,8 +518,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Entities.User", b => { - b.Navigation("Messages"); - b.Navigation("OwnedConversations"); }); #pragma warning restore 612, 618 diff --git a/src/Infrastructure/Data/Seeders/DatabaseSeeder.cs b/src/Infrastructure/Data/Seeders/DatabaseSeeder.cs new file mode 100644 index 0000000..87495ee --- /dev/null +++ b/src/Infrastructure/Data/Seeders/DatabaseSeeder.cs @@ -0,0 +1,97 @@ +using Infrastructure.Data.Contexts; +using Infrastructure.Identity; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace Infrastructure.Data.Seeders; + +/// +/// Database seeder for default roles and admin user +/// +public static class DatabaseSeeder +{ + /// + /// Seed default roles and admin user + /// + public static async Task SeedAsync(DataContext context, RoleManager roleManager, UserManager userManager) + { + // Ensure database is created + await context.Database.MigrateAsync(); + + // Seed roles + await SeedRolesAsync(roleManager); + + // Seed admin user + await SeedAdminUserAsync(userManager, context); + } + + private static async Task SeedRolesAsync(RoleManager roleManager) + { + string[] roles = ["Admin", "User", "Moderator"]; + + foreach (var roleName in roles) + { + var roleExists = await roleManager.RoleExistsAsync(roleName); + if (!roleExists) + { + var role = new ApplicationRole + { + Name = roleName, + Description = $"{roleName} role", + CreatedAt = DateTime.UtcNow + }; + + await roleManager.CreateAsync(role); + } + } + } + + private static async Task SeedAdminUserAsync(UserManager userManager, DataContext context) + { + const string adminEmail = "admin@legalassistant.com"; + const string adminPassword = "Admin@123"; + + var existingAdmin = await userManager.FindByEmailAsync(adminEmail); + if (existingAdmin == null) + { + var adminUserId = Guid.NewGuid(); + + // Create Identity admin user + var identityAdmin = new ApplicationUser + { + Id = adminUserId, + UserName = adminEmail, + Email = adminEmail, + FirstName = "System", + LastName = "Admin", + FullName = "System Admin", + EmailConfirmed = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + var result = await userManager.CreateAsync(identityAdmin, adminPassword); + + if (result.Succeeded) + { + // Add admin role + await userManager.AddToRoleAsync(identityAdmin, "Admin"); + + // Create Domain user + var domainAdmin = new Domain.Entities.User + { + Id = adminUserId, + Email = adminEmail, + FullName = "System Admin", + Roles = ["Admin"], + IsDeleted = false, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + context.DomainUsers.Add(domainAdmin); + await context.SaveChangesAsync(); + } + } + } +} diff --git a/src/Infrastructure/Extensions/IdentityServiceExtensions.cs b/src/Infrastructure/Extensions/IdentityServiceExtensions.cs new file mode 100644 index 0000000..2ca4748 --- /dev/null +++ b/src/Infrastructure/Extensions/IdentityServiceExtensions.cs @@ -0,0 +1,47 @@ +using Infrastructure.Data.Contexts; +using Infrastructure.Identity; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; + +namespace Infrastructure.Extensions; + +/// +/// Identity service configuration extensions +/// +public static class IdentityServiceExtensions +{ + /// + /// Add Identity services to the application + /// + public static IServiceCollection AddIdentityServices(this IServiceCollection services) + { + services.AddIdentity(options => + { + // Password settings + options.Password.RequireDigit = true; + options.Password.RequireLowercase = true; + options.Password.RequireUppercase = true; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequiredLength = 8; + options.Password.RequiredUniqueChars = 1; + + // Lockout settings + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); + options.Lockout.MaxFailedAccessAttempts = 5; + options.Lockout.AllowedForNewUsers = true; + + // User settings + options.User.AllowedUserNameCharacters = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; + options.User.RequireUniqueEmail = true; + + // Sign-in settings + options.SignIn.RequireConfirmedEmail = false; + options.SignIn.RequireConfirmedPhoneNumber = false; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + return services; + } +} diff --git a/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs b/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs index 0fbdd5f..ecba35e 100644 --- a/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs +++ b/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -26,6 +26,9 @@ public static IServiceCollection AddInfrastructureServices( // Database services.AddDatabase(configuration); + // Identity + services.AddIdentityServices(); + // Repositories services.AddRepositories(); @@ -48,6 +51,7 @@ private static IServiceCollection AddDatabase( var connectionString = configuration.GetConnectionString("DefaultConnection") ?? "Host=localhost;Database=legal-assistant;Username=postgres;Password=postgres"; + // Single DbContext with Identity integration services.AddDbContext(options => { options.UseNpgsql(connectionString, npgsqlOptions => @@ -73,11 +77,12 @@ private static IServiceCollection AddDatabase( /// private static IServiceCollection AddRepositories(this IServiceCollection services) { + // Domain User repository services.AddScoped(); - // TODO: Add other repositories here - // services.AddScoped(); - // services.AddScoped(); + // Identity authentication service + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService()); return services; } @@ -93,12 +98,6 @@ private static IServiceCollection AddInfrastructureApplicationServices(this ISer // Domain Events services.AddScoped(); - - // TODO: Add other application services - // services.AddScoped(); - // services.AddScoped(); - // services.AddScoped(); - return services; } diff --git a/src/Infrastructure/Identity/ApplicationRole.cs b/src/Infrastructure/Identity/ApplicationRole.cs new file mode 100644 index 0000000..744c7c2 --- /dev/null +++ b/src/Infrastructure/Identity/ApplicationRole.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Identity; + +namespace Infrastructure.Identity; + +/// +/// Application role for ASP.NET Core Identity +/// +public sealed class ApplicationRole : IdentityRole +{ + /// + /// Role description + /// + public string? Description { get; set; } + + /// + /// Timestamp when the role was created + /// + public DateTime CreatedAt { get; set; } +} diff --git a/src/Infrastructure/Identity/ApplicationUser.cs b/src/Infrastructure/Identity/ApplicationUser.cs new file mode 100644 index 0000000..5f324a2 --- /dev/null +++ b/src/Infrastructure/Identity/ApplicationUser.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Identity; + +namespace Infrastructure.Identity; + +public sealed class ApplicationUser : IdentityUser +{ + public required string FirstName { get; set; } + + public required string LastName { get; set; } + + public required string FullName { get; set; } + + public bool IsDeleted { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime UpdatedAt { get; set; } + + public DateTime? DeletedAt { get; set; } +} diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs new file mode 100644 index 0000000..528d1a3 --- /dev/null +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -0,0 +1,119 @@ +using Application.Interfaces; +using Microsoft.AspNetCore.Identity; + +namespace Infrastructure.Identity; + +/// +/// Identity service implementation wrapping ASP.NET Core Identity +/// +public class IdentityService : IAuthService +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public IdentityService( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + /// + /// Set user password (for registration/password change) + /// + public async Task SetPasswordAsync(Guid userId, string password) + { + var identityUser = await _userManager.FindByIdAsync(userId.ToString()); + if (identityUser == null) + { + return false; + } + + // Remove old password if exists + await _userManager.RemovePasswordAsync(identityUser); + + // Add new password + var result = await _userManager.AddPasswordAsync(identityUser, password); + return result.Succeeded; + } + + /// + /// Create Identity user with password + /// + public async Task CreateIdentityUserAsync(Guid userId, string email, string fullName, string password, List roles) + { + var nameParts = fullName.Split(' ', 2); + var firstName = nameParts.Length > 0 ? nameParts[0] : fullName; + var lastName = nameParts.Length > 1 ? nameParts[1] : string.Empty; + + var identityUser = new ApplicationUser + { + Id = userId, + UserName = email, + Email = email, + FirstName = firstName, + LastName = lastName, + FullName = fullName, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + // Create user with password + var result = await _userManager.CreateAsync(identityUser, password); + if (!result.Succeeded) + { + return false; + } + + // Add roles + if (roles.Any()) + { + await _userManager.AddToRolesAsync(identityUser, roles); + } + + return true; + } + + /// + /// Check if password is correct + /// + public async Task CheckPasswordAsync(string email, string password) + { + var identityUser = await _userManager.FindByEmailAsync(email); + if (identityUser == null || identityUser.IsDeleted) + { + return false; + } + + return await _userManager.CheckPasswordAsync(identityUser, password); + } + + /// + /// Sign in user + /// + public async Task SignInAsync(string email, string password, bool rememberMe = false) + { + var identityUser = await _userManager.FindByEmailAsync(email); + if (identityUser == null || identityUser.IsDeleted) + { + return false; + } + + var result = await _signInManager.PasswordSignInAsync( + identityUser.UserName!, + password, + rememberMe, + lockoutOnFailure: true); + + return result.Succeeded; + } + + /// + /// Sign out user + /// + public async Task SignOutAsync() + { + await _signInManager.SignOutAsync(); + } +} diff --git a/src/Infrastructure/Identity/UserMapper.cs b/src/Infrastructure/Identity/UserMapper.cs new file mode 100644 index 0000000..d463b39 --- /dev/null +++ b/src/Infrastructure/Identity/UserMapper.cs @@ -0,0 +1,80 @@ +using Domain.Entities; + +namespace Infrastructure.Identity; + +/// +/// Maps between Domain.User and ApplicationUser +/// +public static class UserMapper +{ + /// + /// Map ApplicationUser to Domain User + /// + public static User MapToDomainUser(ApplicationUser identityUser) + { + ArgumentNullException.ThrowIfNull(identityUser); + + var user = new User + { + Id = identityUser.Id, + Email = identityUser.Email ?? string.Empty, + FullName = identityUser.FullName, + Roles = [], // Will be loaded separately + IsDeleted = identityUser.IsDeleted, + CreatedAt = identityUser.CreatedAt, + UpdatedAt = identityUser.UpdatedAt, + DeletedAt = identityUser.DeletedAt + }; + + return user; + } + + /// + /// Map Domain User to ApplicationUser (for create/update) + /// + public static ApplicationUser MapToIdentityUser(User domainUser, ApplicationUser? existingUser = null) + { + ArgumentNullException.ThrowIfNull(domainUser); + + var nameParts = domainUser.FullName.Split(' ', 2); + var firstName = nameParts.Length > 0 ? nameParts[0] : domainUser.FullName; + var lastName = nameParts.Length > 1 ? nameParts[1] : string.Empty; + + var identityUser = existingUser ?? new ApplicationUser + { + Id = domainUser.Id, + FirstName = firstName, + LastName = lastName, + FullName = domainUser.FullName, + CreatedAt = domainUser.CreatedAt, + UpdatedAt = domainUser.UpdatedAt ?? DateTime.UtcNow + }; + + if (existingUser != null) + { + identityUser.FirstName = firstName; + identityUser.LastName = lastName; + identityUser.FullName = domainUser.FullName; + identityUser.UpdatedAt = domainUser.UpdatedAt ?? DateTime.UtcNow; + } + + identityUser.UserName = domainUser.Email; + identityUser.Email = domainUser.Email; + identityUser.IsDeleted = domainUser.IsDeleted; + identityUser.DeletedAt = domainUser.DeletedAt; + + return identityUser; + } + + /// + /// Update ApplicationUser roles from Domain User + /// + public static void MapRolesToIdentityUser(User domainUser, ApplicationUser identityUser) + { + ArgumentNullException.ThrowIfNull(domainUser); + ArgumentNullException.ThrowIfNull(identityUser); + + // Roles are managed through UserManager.AddToRoleAsync/RemoveFromRoleAsync + // This method is for reference only + } +} diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index bdcb1da..0986c8f 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -7,13 +7,14 @@ - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Infrastructure/Repositories/UserRepository.cs b/src/Infrastructure/Repositories/UserRepository.cs index 22744da..a382003 100644 --- a/src/Infrastructure/Repositories/UserRepository.cs +++ b/src/Infrastructure/Repositories/UserRepository.cs @@ -22,9 +22,8 @@ public UserRepository(DataContext context) /// public async Task GetByEmailAsync(string email, CancellationToken cancellationToken = default) { - return await _context.Set() - .Where(u => u.Email == email && !u.IsDeleted) - .FirstOrDefaultAsync(cancellationToken); + return await _context.DomainUsers + .FirstOrDefaultAsync(u => u.Email == email, cancellationToken); } /// @@ -32,9 +31,8 @@ public UserRepository(DataContext context) /// public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) { - return await _context.Set() - .Where(u => u.Id == id && !u.IsDeleted) - .FirstOrDefaultAsync(cancellationToken); + return await _context.DomainUsers + .FirstOrDefaultAsync(u => u.Id == id, cancellationToken); } /// @@ -42,7 +40,7 @@ public UserRepository(DataContext context) /// public async Task UpdateAsync(User user, CancellationToken cancellationToken = default) { - _context.Set().Update(user); + _context.DomainUsers.Update(user); await _context.SaveChangesAsync(cancellationToken); } @@ -51,13 +49,8 @@ public async Task UpdateAsync(User user, CancellationToken cancellationToken = d /// public async Task CreateAsync(User user, CancellationToken cancellationToken = default) { - // Add to context - var entityEntry = await _context.Set().AddAsync(user, cancellationToken); - - // Save to database + var entry = await _context.DomainUsers.AddAsync(user, cancellationToken); await _context.SaveChangesAsync(cancellationToken); - - // Return created user with generated ID - return entityEntry.Entity; + return entry.Entity; } } diff --git a/src/Web.Api/Controllers/V1/AuthController.cs b/src/Web.Api/Controllers/V1/AuthController.cs index e567a9b..1fc3111 100644 --- a/src/Web.Api/Controllers/V1/AuthController.cs +++ b/src/Web.Api/Controllers/V1/AuthController.cs @@ -1,4 +1,6 @@ using Application.Features.Auth.Login; +using Application.Features.Auth.Register; +using Domain.Common; using MediatR; using Microsoft.AspNetCore.Mvc; using Web.Api.Models.Responses; @@ -8,7 +10,7 @@ namespace Web.Api.Controllers.V1; /// /// Authentication controller /// -[Route("api/v{version:apiVersion}/[controller]")] +[Route("api/v{version:apiVersion}/auth")] public sealed class AuthController(IMediator mediator) : BaseController { /// @@ -17,7 +19,7 @@ public sealed class AuthController(IMediator mediator) : BaseController /// Login credentials /// Login result with tokens [HttpPost("login")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(LoginResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] public async Task Login([FromBody] LoginCommand command) @@ -26,9 +28,34 @@ public async Task Login([FromBody] LoginCommand command) if (result.IsSuccess) { - return Ok(ApiResponse.CreateSuccess(result.Value!, "Login successful")); + return Ok(result.Value); } - return BadRequest(ApiResponse.CreateFailure(result.Error!.Description)); + return BadRequest(ApiResponse.CreateFailure(result.Error.Description)); + } + + /// + /// User registration + /// + /// Registration information + /// Registration result with user info and tokens + [HttpPost("register")] + [ProducesResponseType(typeof(RegisterResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] + public async Task Register([FromBody] RegisterCommand command) + { + var result = await mediator.Send(command); + + if (result.IsSuccess) + { + return Ok(result.Value!); + } + + return result.Error!.Type switch + { + ErrorType.Conflict => Conflict(ApiResponse.CreateFailure(result.Error.Description)), + _ => BadRequest(ApiResponse.CreateFailure(result.Error.Description)) + }; } } diff --git a/src/Web.Api/Controllers/V1/HealthController.cs b/src/Web.Api/Controllers/V1/HealthController.cs index a18bc30..3e5041c 100644 --- a/src/Web.Api/Controllers/V1/HealthController.cs +++ b/src/Web.Api/Controllers/V1/HealthController.cs @@ -9,7 +9,7 @@ namespace Web.Api.Controllers.V1; /// Health check controller for system monitoring and testing /// [ApiController] -[Route("api/v{version:apiVersion}/[controller]")] +[Route("api/v{version:apiVersion}/health")] [ApiVersion("1.0")] public sealed class HealthController : BaseController { diff --git a/src/Web.Api/Controllers/V1/UsersController.cs b/src/Web.Api/Controllers/V1/UsersController.cs index fab22fb..4672949 100644 --- a/src/Web.Api/Controllers/V1/UsersController.cs +++ b/src/Web.Api/Controllers/V1/UsersController.cs @@ -1,5 +1,4 @@ using Application.Common.Models; -using Application.Features.User.CreateUser; using Application.Features.User.GetUsers; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -10,7 +9,7 @@ namespace Web.Api.Controllers.V1; /// /// Users management controller /// -[Route("api/v{version:apiVersion}/[controller]")] +[Route("api/v{version:apiVersion}/users")] public sealed class UsersController(IMediator mediator) : BaseController { /// @@ -32,29 +31,4 @@ public async Task GetUsers([FromQuery] GetUsersQuery query) return BadRequest(ApiResponse.CreateFailure(result.Error!.Description)); } - - /// - /// Create new user - /// - /// User creation data - /// Created user information - [HttpPost] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status201Created)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] - public async Task CreateUser([FromBody] CreateUserCommand command) - { - var result = await mediator.Send(command); - - if (result.IsSuccess) - { - return CreatedAtAction( - nameof(GetUsers), - new { id = result.Value!.Id }, - ApiResponse.CreateSuccess(result.Value, "User created successfully")); - } - - return BadRequest(ApiResponse.CreateFailure(result.Error!.Description)); - } } diff --git a/src/Web.Api/Extensions/AuthenticationExtensions.cs b/src/Web.Api/Extensions/AuthenticationExtensions.cs new file mode 100644 index 0000000..09b9ed3 --- /dev/null +++ b/src/Web.Api/Extensions/AuthenticationExtensions.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; + +namespace Web.Api.Extensions; + +/// +/// Authentication configuration extensions +/// +public static class AuthenticationExtensions +{ + /// + /// Add JWT Bearer authentication + /// + public static IServiceCollection AddJwtAuthentication( + this IServiceCollection services, + IConfiguration configuration) + { + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + var jwtSettings = configuration.GetSection("JwtSettings"); + var secretKey = jwtSettings["SecretKey"] ?? "YourSuperSecretKeyThatIsAtLeast32CharactersLong!!"; + + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)), + ValidateIssuer = true, + ValidIssuer = jwtSettings["Issuer"] ?? "LegalAssistant", + ValidateAudience = true, + ValidAudience = jwtSettings["Audience"] ?? "LegalAssistantUsers", + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; + }); + + services.AddAuthorization(); + + return services; + } +} diff --git a/src/Web.Api/Program.cs b/src/Web.Api/Program.cs index 76514f1..0257397 100644 --- a/src/Web.Api/Program.cs +++ b/src/Web.Api/Program.cs @@ -1,7 +1,8 @@ +using Infrastructure.Data.Contexts; +using Infrastructure.Data.Seeders; using Infrastructure.Extensions; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.IdentityModel.Tokens; -using System.Text; +using Infrastructure.Identity; +using Microsoft.AspNetCore.Identity; using Web.Api.Extensions; using Web.Api.Middleware; @@ -16,32 +17,8 @@ // Add Application services (MediatR, Behaviors, Validation) builder.Services.AddApplicationServices(); -// Add Authentication -builder.Services.AddAuthentication(options => -{ - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; -}) -.AddJwtBearer(options => -{ - var jwtSettings = builder.Configuration.GetSection("JwtSettings"); - var secretKey = jwtSettings["SecretKey"] ?? "YourSuperSecretKeyThatIsAtLeast32CharactersLong!!"; - - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)), - ValidateIssuer = true, - ValidIssuer = jwtSettings["Issuer"] ?? "LegalAssistant", - ValidateAudience = true, - ValidAudience = jwtSettings["Audience"] ?? "LegalAssistantUsers", - ValidateLifetime = true, - ClockSkew = TimeSpan.Zero - }; -}); - -// Add Authorization -builder.Services.AddAuthorization(); +// Add Authentication & Authorization +builder.Services.AddJwtAuthentication(builder.Configuration); // Add API services builder.Services.AddApiVersioningConfiguration(); @@ -87,32 +64,31 @@ // Automatically redirect to Swagger UI app.MapGet("/", () => Results.Redirect("/swagger")); -// Ensure database is created -await EnsureDatabaseCreatedAsync(app); +// Seed database with default data +await SeedDatabaseAsync(app); await app.RunAsync(); /// -/// Ensure database is created and optionally seeded +/// Seed database with default roles and admin user /// -static async Task EnsureDatabaseCreatedAsync(WebApplication app) +static async Task SeedDatabaseAsync(WebApplication app) { using var scope = app.Services.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); + var services = scope.ServiceProvider; try { - // Create database if it doesn't exist - await context.Database.EnsureCreatedAsync(); + var context = services.GetRequiredService(); + var roleManager = services.GetRequiredService>(); + var userManager = services.GetRequiredService>(); - // TODO: Add data seeding here if needed - // await SeedDataAsync(context); + await DatabaseSeeder.SeedAsync(context, roleManager, userManager); - app.Logger.LogInformation("Database ensured and ready"); + app.Logger.LogInformation("Database seeded successfully"); } catch (Exception ex) { - app.Logger.LogError(ex, "An error occurred while ensuring the database was created"); - throw new InvalidOperationException("Failed to initialize database during application startup", ex); + app.Logger.LogError(ex, "An error occurred while seeding the database"); } }