From 1704e255fff192fe1c085cefeebfbe653375b85f Mon Sep 17 00:00:00 2001 From: Veljko Tosic Date: Thu, 12 Feb 2026 19:46:42 +0100 Subject: [PATCH 01/11] Nuget packages --- dotnet/AipsCore/AipsCore.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet/AipsCore/AipsCore.csproj b/dotnet/AipsCore/AipsCore.csproj index d062ba1..fbd60df 100644 --- a/dotnet/AipsCore/AipsCore.csproj +++ b/dotnet/AipsCore/AipsCore.csproj @@ -7,6 +7,8 @@ + + all From 9cfc23a5d5a5871b94d8f33eac0a8b5fd744fec5 Mon Sep 17 00:00:00 2001 From: Veljko Tosic Date: Thu, 12 Feb 2026 19:47:14 +0100 Subject: [PATCH 02/11] Login errors --- .../Domain/Models/User/Validation/UserErrors.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs b/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs index 558d7a6..8f8b122 100644 --- a/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs +++ b/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs @@ -1,9 +1,24 @@ using AipsCore.Domain.Abstract.Validation; +using AipsCore.Domain.Common.Validation; using AipsCore.Domain.Models.User.ValueObjects; namespace AipsCore.Domain.Models.User.Validation; public class UserErrors : AbstractErrors { + public static ValidationError LoginErrorUserNotFoundByEmail(string email) + { + string code = "login_error_user_not_found_by_email"; + string message = $"User with email '{email}' not found"; + + return CreateValidationError(code, message); + } + public static ValidationError LoginErrorIncorrectPassword() + { + string code = "login_error_incorrect_password"; + string message = $"Incorrect password provided"; + + return CreateValidationError(code, message); + } } \ No newline at end of file From 95d5f9da87b85d375653167c1cdc29cf46600bb1 Mon Sep 17 00:00:00 2001 From: Veljko Tosic Date: Thu, 12 Feb 2026 19:47:38 +0100 Subject: [PATCH 03/11] Auth methods --- dotnet/AipsCore/Domain/Models/User/External/IUserRepository.cs | 3 ++- dotnet/AipsCore/Domain/Models/User/External/LoginResult.cs | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 dotnet/AipsCore/Domain/Models/User/External/LoginResult.cs diff --git a/dotnet/AipsCore/Domain/Models/User/External/IUserRepository.cs b/dotnet/AipsCore/Domain/Models/User/External/IUserRepository.cs index 7a9f154..edc3107 100644 --- a/dotnet/AipsCore/Domain/Models/User/External/IUserRepository.cs +++ b/dotnet/AipsCore/Domain/Models/User/External/IUserRepository.cs @@ -5,5 +5,6 @@ namespace AipsCore.Domain.Models.User.External; public interface IUserRepository : IAbstractRepository { - + Task SignUpWithPasswordAsync(User user, string password, CancellationToken cancellationToken = default); + Task LoginWithEmailAndPasswordAsync(string email, string password, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/User/External/LoginResult.cs b/dotnet/AipsCore/Domain/Models/User/External/LoginResult.cs new file mode 100644 index 0000000..3988a20 --- /dev/null +++ b/dotnet/AipsCore/Domain/Models/User/External/LoginResult.cs @@ -0,0 +1,3 @@ +namespace AipsCore.Domain.Models.User.External; + +public record LoginResult(User User, IList Roles); \ No newline at end of file From c7e7735075ec2f8d05709bcd2e6e6ea64888a5ef Mon Sep 17 00:00:00 2001 From: Veljko Tosic Date: Thu, 12 Feb 2026 19:49:37 +0100 Subject: [PATCH 04/11] Services, service registration and dependency injection for auth --- .../Abstract/UserContext/ITokenProvider.cs | 8 +++ .../Application/Authentication/Token.cs | 3 + .../DI/AipsRegistrationExtensions.cs | 2 +- .../DI/UserContextRegistrationExtension.cs | 58 ++++++++++++++++++- .../Authentication/HttpUserContext.cs | 35 +++++++++++ .../Persistence/Authentication/JwtSettings.cs | 9 +++ .../Authentication/JwtTokenProvider.cs | 44 ++++++++++++++ .../Persistence/Db/AipsDbContext.cs | 7 ++- dotnet/AipsCore/Infrastructure/UserContext.cs | 14 ----- 9 files changed, 160 insertions(+), 20 deletions(-) create mode 100644 dotnet/AipsCore/Application/Abstract/UserContext/ITokenProvider.cs create mode 100644 dotnet/AipsCore/Application/Authentication/Token.cs create mode 100644 dotnet/AipsCore/Infrastructure/Persistence/Authentication/HttpUserContext.cs create mode 100644 dotnet/AipsCore/Infrastructure/Persistence/Authentication/JwtSettings.cs create mode 100644 dotnet/AipsCore/Infrastructure/Persistence/Authentication/JwtTokenProvider.cs delete mode 100644 dotnet/AipsCore/Infrastructure/UserContext.cs diff --git a/dotnet/AipsCore/Application/Abstract/UserContext/ITokenProvider.cs b/dotnet/AipsCore/Application/Abstract/UserContext/ITokenProvider.cs new file mode 100644 index 0000000..7839f5f --- /dev/null +++ b/dotnet/AipsCore/Application/Abstract/UserContext/ITokenProvider.cs @@ -0,0 +1,8 @@ +using AipsCore.Domain.Models.User; + +namespace AipsCore.Application.Abstract.UserContext; + +public interface ITokenProvider +{ + string Generate(User user, IList roles); +} \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Authentication/Token.cs b/dotnet/AipsCore/Application/Authentication/Token.cs new file mode 100644 index 0000000..85915f3 --- /dev/null +++ b/dotnet/AipsCore/Application/Authentication/Token.cs @@ -0,0 +1,3 @@ +namespace AipsCore.Application.Authentication; + +public record Token(string Value); \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/DI/AipsRegistrationExtensions.cs b/dotnet/AipsCore/Infrastructure/DI/AipsRegistrationExtensions.cs index a213b5d..351ea14 100644 --- a/dotnet/AipsCore/Infrastructure/DI/AipsRegistrationExtensions.cs +++ b/dotnet/AipsCore/Infrastructure/DI/AipsRegistrationExtensions.cs @@ -14,7 +14,7 @@ public static class AipsRegistrationExtensions services.AddPersistence(configuration); - services.AddUserContext(); + services.AddUserContext(configuration); return services; } diff --git a/dotnet/AipsCore/Infrastructure/DI/UserContextRegistrationExtension.cs b/dotnet/AipsCore/Infrastructure/DI/UserContextRegistrationExtension.cs index 7513690..f051b43 100644 --- a/dotnet/AipsCore/Infrastructure/DI/UserContextRegistrationExtension.cs +++ b/dotnet/AipsCore/Infrastructure/DI/UserContextRegistrationExtension.cs @@ -1,13 +1,67 @@ +using System.Text; using AipsCore.Application.Abstract.UserContext; +using AipsCore.Infrastructure.Persistence.Authentication; +using AipsCore.Infrastructure.Persistence.Db; +using AipsCore.Infrastructure.Persistence.User; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; namespace AipsCore.Infrastructure.DI; public static class UserContextRegistrationExtension { - public static IServiceCollection AddUserContext(this IServiceCollection services) + public static IServiceCollection AddUserContext(this IServiceCollection services, IConfiguration configuration) { - services.AddTransient(); + var jwtSettings = new JwtSettings + { + Issuer = configuration["JWT_ISSUER"]!, + Audience = configuration["JWT_AUDIENCE"]!, + Key = configuration["JWT_KEY"]!, + ExpirationMinutes = int.Parse(configuration["JWT_EXPIRATION_MINUTES"] ?? "60") + }; + + services.AddSingleton(jwtSettings); + + services.AddHttpContextAccessor(); + + services.AddIdentityCore(options => + { + options.Password.RequiredLength = 8; + options.Password.RequireDigit = true; + options.Password.RequireLowercase = true; + options.Password.RequireUppercase = true; + options.Password.RequireNonAlphanumeric = true; + + options.User.RequireUniqueEmail = true; + }) + .AddRoles>() + .AddEntityFrameworkStores() + .AddSignInManager() + .AddDefaultTokenProviders(); + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtSettings.Issuer, + ValidAudience = jwtSettings.Audience, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(jwtSettings.Key)) + }; + }); + + services.AddAuthorization(); + + services.AddTransient(); + services.AddTransient(); return services; } diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Authentication/HttpUserContext.cs b/dotnet/AipsCore/Infrastructure/Persistence/Authentication/HttpUserContext.cs new file mode 100644 index 0000000..a55d734 --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/Persistence/Authentication/HttpUserContext.cs @@ -0,0 +1,35 @@ +using System.Security.Claims; +using AipsCore.Application.Abstract.UserContext; +using AipsCore.Domain.Models.User.ValueObjects; +using Microsoft.AspNetCore.Http; + +namespace AipsCore.Infrastructure.Persistence.Authentication; + +public class HttpUserContext : IUserContext +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public HttpUserContext(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public UserId GetCurrentUserId() + { + var user = _httpContextAccessor.HttpContext?.User; + + if (user is null || !user.Identity!.IsAuthenticated) + { + throw new UnauthorizedAccessException("User is not authenticated"); + } + + var userIdClaim = user.FindFirst(ClaimTypes.NameIdentifier); + + if (userIdClaim is null) + { + throw new UnauthorizedAccessException("User id claim not found"); + } + + return new UserId(userIdClaim.Value); + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Authentication/JwtSettings.cs b/dotnet/AipsCore/Infrastructure/Persistence/Authentication/JwtSettings.cs new file mode 100644 index 0000000..b095aef --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/Persistence/Authentication/JwtSettings.cs @@ -0,0 +1,9 @@ +namespace AipsCore.Infrastructure.Persistence.Authentication; + +public sealed class JwtSettings +{ + public string Issuer { get; init; } = null!; + public string Audience { get; init; } = null!; + public string Key { get; init; } = null!; + public int ExpirationMinutes { get; init; } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Authentication/JwtTokenProvider.cs b/dotnet/AipsCore/Infrastructure/Persistence/Authentication/JwtTokenProvider.cs new file mode 100644 index 0000000..c80883b --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/Persistence/Authentication/JwtTokenProvider.cs @@ -0,0 +1,44 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using AipsCore.Application.Abstract.UserContext; +using Microsoft.IdentityModel.Tokens; + +namespace AipsCore.Infrastructure.Persistence.Authentication; + +public class JwtTokenProvider : ITokenProvider +{ + private readonly JwtSettings _jwtSettings; + + public JwtTokenProvider(JwtSettings jwtSettings) + { + _jwtSettings = jwtSettings; + } + + public string Generate(Domain.Models.User.User user, IList roles) + { + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, user.Id.IdValue), + new Claim(ClaimTypes.Email, user.Email.EmailValue) + }; + + foreach (var role in roles) + { + claims.Add(new Claim(ClaimTypes.Role, role)); + } + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Key)); + + var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: _jwtSettings.Issuer, + audience: _jwtSettings.Audience, + claims: claims, + expires: DateTime.UtcNow.AddMinutes(_jwtSettings.ExpirationMinutes), + signingCredentials: credentials); + + return new JwtSecurityTokenHandler().WriteToken(token); + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Db/AipsDbContext.cs b/dotnet/AipsCore/Infrastructure/Persistence/Db/AipsDbContext.cs index cbe5e5d..5aff8f0 100644 --- a/dotnet/AipsCore/Infrastructure/Persistence/Db/AipsDbContext.cs +++ b/dotnet/AipsCore/Infrastructure/Persistence/Db/AipsDbContext.cs @@ -1,10 +1,11 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace AipsCore.Infrastructure.Persistence.Db; -public class AipsDbContext : DbContext +public class AipsDbContext : IdentityDbContext, Guid> { - public DbSet Users { get; set; } public DbSet Whiteboards { get; set; } public DbSet Shapes { get; set; } public DbSet WhiteboardMemberships { get; set; } diff --git a/dotnet/AipsCore/Infrastructure/UserContext.cs b/dotnet/AipsCore/Infrastructure/UserContext.cs deleted file mode 100644 index 41bc36e..0000000 --- a/dotnet/AipsCore/Infrastructure/UserContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using AipsCore.Application.Abstract.UserContext; -using AipsCore.Domain.Models.User.ValueObjects; - -namespace AipsCore.Infrastructure; - -public class UserContext : IUserContext -{ - public UserId GetCurrentUserId() - { - return new UserId(new Guid("52a1810c-802f-48b0-a74c-7b517807e392").ToString()); - } -} - -//Ovo je samo trenutno resenje \ No newline at end of file From 072d4e3f4646ac6d8f9213c72e64660ad8d10757 Mon Sep 17 00:00:00 2001 From: Veljko Tosic Date: Thu, 12 Feb 2026 19:50:26 +0100 Subject: [PATCH 05/11] Modified user entity for identity --- .../Infrastructure/Persistence/User/User.cs | 14 +---- .../Persistence/User/UserRepository.cs | 55 +++++++++++++++++-- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/dotnet/AipsCore/Infrastructure/Persistence/User/User.cs b/dotnet/AipsCore/Infrastructure/Persistence/User/User.cs index 3d6f3a2..0bc6504 100644 --- a/dotnet/AipsCore/Infrastructure/Persistence/User/User.cs +++ b/dotnet/AipsCore/Infrastructure/Persistence/User/User.cs @@ -1,21 +1,11 @@ using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Identity; namespace AipsCore.Infrastructure.Persistence.User; -public class User +public class User : IdentityUser { - [Key] - public Guid Id { get; set; } - - [Required] - [MaxLength(255)] - public string Username { get; set; } = null!; - - [Required] - [MaxLength(255)] - public string Email { get; set; } = null!; - public DateTime CreatedAt { get; set; } public DateTime? DeletedAt { get; set; } diff --git a/dotnet/AipsCore/Infrastructure/Persistence/User/UserRepository.cs b/dotnet/AipsCore/Infrastructure/Persistence/User/UserRepository.cs index 2e4a126..e4f6965 100644 --- a/dotnet/AipsCore/Infrastructure/Persistence/User/UserRepository.cs +++ b/dotnet/AipsCore/Infrastructure/Persistence/User/UserRepository.cs @@ -1,15 +1,21 @@ +using AipsCore.Domain.Common.Validation; using AipsCore.Domain.Models.User.External; +using AipsCore.Domain.Models.User.Validation; using AipsCore.Domain.Models.User.ValueObjects; using AipsCore.Infrastructure.Persistence.Abstract; using AipsCore.Infrastructure.Persistence.Db; +using Microsoft.AspNetCore.Identity; namespace AipsCore.Infrastructure.Persistence.User; public class UserRepository : AbstractRepository, IUserRepository { - public UserRepository(AipsDbContext context) + private readonly UserManager _userManager; + + public UserRepository(AipsDbContext context, UserManager userManager) : base(context) { + _userManager = userManager; } protected override Domain.Models.User.User MapToModel(User entity) @@ -17,7 +23,7 @@ public class UserRepository : AbstractRepository e.Description)); + throw new Exception($"User registration failed: {errors}"); + } + + //Realno nebitno, ali postoji infrastruktura ako treba da se koristi kasnije + //await _userManager.AddToRoleAsync(entity, "User"); + } + + public async Task LoginWithEmailAndPasswordAsync(string email, string password, CancellationToken cancellationToken = default) + { + var entity = await _userManager.FindByEmailAsync(email); + + if (entity is null) + { + throw new ValidationException(UserErrors.LoginErrorUserNotFoundByEmail(email)); + } + + var isPasswordValid = await _userManager.CheckPasswordAsync(entity, password); + + if (!isPasswordValid) + { + throw new ValidationException(UserErrors.LoginErrorIncorrectPassword()); + } + + var roles = await _userManager.GetRolesAsync(entity); + + return new LoginResult(MapToModel(entity), roles); + } } \ No newline at end of file From df2130ec249204303ffb7b09877041df3d087379 Mon Sep 17 00:00:00 2001 From: Veljko Tosic Date: Thu, 12 Feb 2026 19:51:27 +0100 Subject: [PATCH 06/11] Modified commands to use auth and current user --- .../User/Command/LogIn/LogInUserCommand.cs | 6 ++++ .../Command/LogIn/LogInUserCommandHandler.cs | 28 +++++++++++++++++++ .../User/Command/SignUp/SignUpUserCommand.cs | 9 ++++++ .../SignUp/SignUpUserCommandHandler.cs | 27 ++++++++++++++++++ .../CreateWhiteboardCommand.cs | 1 - .../CreateWhiteboardCommandHandler.cs | 11 ++++++-- 6 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommand.cs create mode 100644 dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommandHandler.cs create mode 100644 dotnet/AipsCore/Application/Models/User/Command/SignUp/SignUpUserCommand.cs create mode 100644 dotnet/AipsCore/Application/Models/User/Command/SignUp/SignUpUserCommandHandler.cs diff --git a/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommand.cs b/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommand.cs new file mode 100644 index 0000000..3a2312d --- /dev/null +++ b/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommand.cs @@ -0,0 +1,6 @@ +using AipsCore.Application.Abstract.Command; +using AipsCore.Application.Authentication; + +namespace AipsCore.Application.Models.User.Command.LogIn; + +public record LogInUserCommand(string Email, string Password) : ICommand; \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommandHandler.cs b/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommandHandler.cs new file mode 100644 index 0000000..2316407 --- /dev/null +++ b/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommandHandler.cs @@ -0,0 +1,28 @@ +using AipsCore.Application.Abstract.Command; +using AipsCore.Application.Abstract.UserContext; +using AipsCore.Application.Authentication; +using AipsCore.Domain.Abstract; +using AipsCore.Domain.Models.User.External; + +namespace AipsCore.Application.Models.User.Command.LogIn; + +public class LogInUserCommandHandler : ICommandHandler +{ + private readonly IUserRepository _userRepository; + private readonly ITokenProvider _tokenProvider; + private readonly IUnitOfWork _unitOfWork; + + public LogInUserCommandHandler(IUserRepository userRepository, ITokenProvider tokenProvider, IUnitOfWork unitOfWork) + { + _userRepository = userRepository; + _tokenProvider = tokenProvider; + _unitOfWork = unitOfWork; + } + + public async Task Handle(LogInUserCommand command, CancellationToken cancellationToken = default) + { + var loginResult = await _userRepository.LoginWithEmailAndPasswordAsync(command.Email, command.Password, cancellationToken); + + return new Token(_tokenProvider.Generate(loginResult.User, loginResult.Roles)); + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Models/User/Command/SignUp/SignUpUserCommand.cs b/dotnet/AipsCore/Application/Models/User/Command/SignUp/SignUpUserCommand.cs new file mode 100644 index 0000000..4f742f0 --- /dev/null +++ b/dotnet/AipsCore/Application/Models/User/Command/SignUp/SignUpUserCommand.cs @@ -0,0 +1,9 @@ +using AipsCore.Application.Abstract.Command; +using AipsCore.Domain.Models.User.ValueObjects; + +namespace AipsCore.Application.Models.User.Command.SignUp; + +public record SignUpUserCommand( + string Username, + string Email, + string Password) : ICommand; \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Models/User/Command/SignUp/SignUpUserCommandHandler.cs b/dotnet/AipsCore/Application/Models/User/Command/SignUp/SignUpUserCommandHandler.cs new file mode 100644 index 0000000..722d811 --- /dev/null +++ b/dotnet/AipsCore/Application/Models/User/Command/SignUp/SignUpUserCommandHandler.cs @@ -0,0 +1,27 @@ +using AipsCore.Application.Abstract.Command; +using AipsCore.Domain.Abstract; +using AipsCore.Domain.Models.User.External; +using AipsCore.Domain.Models.User.ValueObjects; + +namespace AipsCore.Application.Models.User.Command.SignUp; + +public class SignUpUserCommandHandler : ICommandHandler +{ + private readonly IUserRepository _userRepository; + private readonly IUnitOfWork _unitOfWork; + + public SignUpUserCommandHandler(IUserRepository userRepository, IUnitOfWork unitOfWork) + { + _userRepository = userRepository; + _unitOfWork = unitOfWork; + } + + public async Task Handle(SignUpUserCommand command, CancellationToken cancellationToken = default) + { + var user = Domain.Models.User.User.Create(command.Email, command.Username); + + await _userRepository.SignUpWithPasswordAsync(user, command.Password, cancellationToken); + + return user.Id; + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Models/Whiteboard/Command/CreateWhiteboard/CreateWhiteboardCommand.cs b/dotnet/AipsCore/Application/Models/Whiteboard/Command/CreateWhiteboard/CreateWhiteboardCommand.cs index 3916524..03d51f7 100644 --- a/dotnet/AipsCore/Application/Models/Whiteboard/Command/CreateWhiteboard/CreateWhiteboardCommand.cs +++ b/dotnet/AipsCore/Application/Models/Whiteboard/Command/CreateWhiteboard/CreateWhiteboardCommand.cs @@ -6,7 +6,6 @@ using AipsCore.Domain.Models.Whiteboard.ValueObjects; namespace AipsCore.Application.Models.Whiteboard.Command.CreateWhiteboard; public record CreateWhiteboardCommand( - string OwnerId, string Title, int MaxParticipants, WhiteboardJoinPolicy JoinPolicy) diff --git a/dotnet/AipsCore/Application/Models/Whiteboard/Command/CreateWhiteboard/CreateWhiteboardCommandHandler.cs b/dotnet/AipsCore/Application/Models/Whiteboard/Command/CreateWhiteboard/CreateWhiteboardCommandHandler.cs index 67adbae..4e69a6a 100644 --- a/dotnet/AipsCore/Application/Models/Whiteboard/Command/CreateWhiteboard/CreateWhiteboardCommandHandler.cs +++ b/dotnet/AipsCore/Application/Models/Whiteboard/Command/CreateWhiteboard/CreateWhiteboardCommandHandler.cs @@ -1,4 +1,5 @@ using AipsCore.Application.Abstract.Command; +using AipsCore.Application.Abstract.UserContext; using AipsCore.Domain.Abstract; using AipsCore.Domain.Models.Whiteboard.External; using AipsCore.Domain.Models.Whiteboard.ValueObjects; @@ -8,20 +9,24 @@ namespace AipsCore.Application.Models.Whiteboard.Command.CreateWhiteboard; public class CreateWhiteboardCommandHandler : ICommandHandler { private readonly IWhiteboardRepository _whiteboardRepository; + private readonly IUserContext _userContext; private readonly IUnitOfWork _unitOfWork; - - public CreateWhiteboardCommandHandler(IWhiteboardRepository whiteboardRepository, IUnitOfWork unitOfWork) + + public CreateWhiteboardCommandHandler(IWhiteboardRepository whiteboardRepository, IUserContext userContext, IUnitOfWork unitOfWork) { _whiteboardRepository = whiteboardRepository; + _userContext = userContext; _unitOfWork = unitOfWork; } public async Task Handle(CreateWhiteboardCommand command, CancellationToken cancellationToken = default) { var whiteboardCode = await WhiteboardCode.GenerateUniqueAsync(_whiteboardRepository); + + var ownerId = _userContext.GetCurrentUserId(); var whiteboard = Domain.Models.Whiteboard.Whiteboard.Create( - command.OwnerId, + ownerId.IdValue, whiteboardCode.CodeValue, command.Title, command.MaxParticipants, From 589ffa4d8ea285afe67ef8164ca0e8a7d6503902 Mon Sep 17 00:00:00 2001 From: Veljko Tosic Date: Thu, 12 Feb 2026 19:51:42 +0100 Subject: [PATCH 07/11] Migrations with added auth --- .../20260212170426_AddedAuth.Designer.cs | 471 +++++++++++++++++ .../Db/Migrations/20260212170426_AddedAuth.cs | 482 ++++++++++++++++++ .../Migrations/AipsDbContextModelSnapshot.cs | 241 ++++++++- 3 files changed, 1186 insertions(+), 8 deletions(-) create mode 100644 dotnet/AipsCore/Infrastructure/Persistence/Db/Migrations/20260212170426_AddedAuth.Designer.cs create mode 100644 dotnet/AipsCore/Infrastructure/Persistence/Db/Migrations/20260212170426_AddedAuth.cs diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Db/Migrations/20260212170426_AddedAuth.Designer.cs b/dotnet/AipsCore/Infrastructure/Persistence/Db/Migrations/20260212170426_AddedAuth.Designer.cs new file mode 100644 index 0000000..8bc1dd7 --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/Persistence/Db/Migrations/20260212170426_AddedAuth.Designer.cs @@ -0,0 +1,471 @@ +// +using System; +using AipsCore.Infrastructure.Persistence.Db; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace AipsCore.Infrastructure.Persistence.Db.Migrations +{ + [DbContext(typeof(AipsDbContext))] + [Migration("20260212170426_AddedAuth")] + partial class AddedAuth + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Shape.Shape", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorId") + .HasColumnType("uuid"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("EndPositionX") + .HasColumnType("integer"); + + b.Property("EndPositionY") + .HasColumnType("integer"); + + b.Property("PositionX") + .HasColumnType("integer"); + + b.Property("PositionY") + .HasColumnType("integer"); + + b.Property("TextSize") + .HasColumnType("integer"); + + b.Property("TextValue") + .HasColumnType("text"); + + b.Property("Thickness") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("WhiteboardId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("WhiteboardId"); + + b.ToTable("Shapes"); + }); + + modelBuilder.Entity("AipsCore.Infrastructure.Persistence.User.User", 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("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("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("JoinPolicy") + .HasColumnType("integer"); + + b.Property("MaxParticipants") + .HasColumnType("integer"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Whiteboards"); + }); + + modelBuilder.Entity("AipsCore.Infrastructure.Persistence.WhiteboardMembership.WhiteboardMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanJoin") + .HasColumnType("boolean"); + + b.Property("EditingEnabled") + .HasColumnType("boolean"); + + b.Property("IsBanned") + .HasColumnType("boolean"); + + b.Property("LastInteractedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("WhiteboardId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("WhiteboardId"); + + b.ToTable("WhiteboardMemberships"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .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("AspNetRoles", (string)null); + }); + + 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("AspNetRoleClaims", (string)null); + }); + + 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("AspNetUserClaims", (string)null); + }); + + 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("AspNetUserLogins", (string)null); + }); + + 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("AspNetUserRoles", (string)null); + }); + + 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("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Shape.Shape", b => + { + b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "Author") + .WithMany("Shapes") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", "Whiteboard") + .WithMany("Shapes") + .HasForeignKey("WhiteboardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Whiteboard"); + }); + + modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", b => + { + b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "Owner") + .WithMany("Whiteboards") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("AipsCore.Infrastructure.Persistence.WhiteboardMembership.WhiteboardMembership", b => + { + b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "User") + .WithMany("Memberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", "Whiteboard") + .WithMany("Memberships") + .HasForeignKey("WhiteboardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("Whiteboard"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AipsCore.Infrastructure.Persistence.User.User", b => + { + b.Navigation("Memberships"); + + b.Navigation("Shapes"); + + b.Navigation("Whiteboards"); + }); + + modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", b => + { + b.Navigation("Memberships"); + + b.Navigation("Shapes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Db/Migrations/20260212170426_AddedAuth.cs b/dotnet/AipsCore/Infrastructure/Persistence/Db/Migrations/20260212170426_AddedAuth.cs new file mode 100644 index 0000000..5e15cc6 --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/Persistence/Db/Migrations/20260212170426_AddedAuth.cs @@ -0,0 +1,482 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace AipsCore.Infrastructure.Persistence.Db.Migrations +{ + /// + public partial class AddedAuth : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Shapes_Users_AuthorId", + table: "Shapes"); + + migrationBuilder.DropForeignKey( + name: "FK_WhiteboardMemberships_Users_UserId", + table: "WhiteboardMemberships"); + + migrationBuilder.DropForeignKey( + name: "FK_Whiteboards_Users_OwnerId", + table: "Whiteboards"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Users", + table: "Users"); + + migrationBuilder.RenameTable( + name: "Users", + newName: "AspNetUsers"); + + migrationBuilder.RenameColumn( + name: "Username", + table: "AspNetUsers", + newName: "UserName"); + + migrationBuilder.AlterColumn( + name: "UserName", + table: "AspNetUsers", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(255)", + oldMaxLength: 255); + + migrationBuilder.AlterColumn( + name: "Email", + table: "AspNetUsers", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(255)", + oldMaxLength: 255); + + migrationBuilder.AddColumn( + name: "AccessFailedCount", + table: "AspNetUsers", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ConcurrencyStamp", + table: "AspNetUsers", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "EmailConfirmed", + table: "AspNetUsers", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "LockoutEnabled", + table: "AspNetUsers", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "LockoutEnd", + table: "AspNetUsers", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "NormalizedEmail", + table: "AspNetUsers", + type: "character varying(256)", + maxLength: 256, + nullable: true); + + migrationBuilder.AddColumn( + name: "NormalizedUserName", + table: "AspNetUsers", + type: "character varying(256)", + maxLength: 256, + nullable: true); + + migrationBuilder.AddColumn( + name: "PasswordHash", + table: "AspNetUsers", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "PhoneNumber", + table: "AspNetUsers", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "PhoneNumberConfirmed", + table: "AspNetUsers", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "SecurityStamp", + table: "AspNetUsers", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "TwoFactorEnabled", + table: "AspNetUsers", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddPrimaryKey( + name: "PK_AspNetUsers", + table: "AspNetUsers", + column: "Id"); + + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "uuid", 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_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + 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_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + 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_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + 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_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + 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_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "uuid", nullable: false), + RoleId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.AddForeignKey( + name: "FK_Shapes_AspNetUsers_AuthorId", + table: "Shapes", + column: "AuthorId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_WhiteboardMemberships_AspNetUsers_UserId", + table: "WhiteboardMemberships", + column: "UserId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Whiteboards_AspNetUsers_OwnerId", + table: "Whiteboards", + column: "OwnerId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Shapes_AspNetUsers_AuthorId", + table: "Shapes"); + + migrationBuilder.DropForeignKey( + name: "FK_WhiteboardMemberships_AspNetUsers_UserId", + table: "WhiteboardMemberships"); + + migrationBuilder.DropForeignKey( + name: "FK_Whiteboards_AspNetUsers_OwnerId", + table: "Whiteboards"); + + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropPrimaryKey( + name: "PK_AspNetUsers", + table: "AspNetUsers"); + + migrationBuilder.DropIndex( + name: "EmailIndex", + table: "AspNetUsers"); + + migrationBuilder.DropIndex( + name: "UserNameIndex", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "AccessFailedCount", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "ConcurrencyStamp", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "EmailConfirmed", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "LockoutEnabled", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "LockoutEnd", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "NormalizedEmail", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "NormalizedUserName", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "PasswordHash", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "PhoneNumber", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "PhoneNumberConfirmed", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "SecurityStamp", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "TwoFactorEnabled", + table: "AspNetUsers"); + + migrationBuilder.RenameTable( + name: "AspNetUsers", + newName: "Users"); + + migrationBuilder.RenameColumn( + name: "UserName", + table: "Users", + newName: "Username"); + + migrationBuilder.AlterColumn( + name: "Username", + table: "Users", + type: "character varying(255)", + maxLength: 255, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Email", + table: "Users", + type: "character varying(255)", + maxLength: 255, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AddPrimaryKey( + name: "PK_Users", + table: "Users", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Shapes_Users_AuthorId", + table: "Shapes", + column: "AuthorId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_WhiteboardMemberships_Users_UserId", + table: "WhiteboardMemberships", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Whiteboards_Users_OwnerId", + table: "Whiteboards", + column: "OwnerId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Db/Migrations/AipsDbContextModelSnapshot.cs b/dotnet/AipsCore/Infrastructure/Persistence/Db/Migrations/AipsDbContextModelSnapshot.cs index e9468c5..a0c1825 100644 --- a/dotnet/AipsCore/Infrastructure/Persistence/Db/Migrations/AipsDbContextModelSnapshot.cs +++ b/dotnet/AipsCore/Infrastructure/Persistence/Db/Migrations/AipsDbContextModelSnapshot.cs @@ -78,6 +78,13 @@ namespace AipsCore.Infrastructure.Persistence.Db.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -85,18 +92,55 @@ namespace AipsCore.Infrastructure.Persistence.Db.Migrations .HasColumnType("timestamp with time zone"); b.Property("Email") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); - b.Property("Username") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + 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("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.HasKey("Id"); - b.ToTable("Users"); + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); }); modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", b => @@ -173,6 +217,136 @@ namespace AipsCore.Infrastructure.Persistence.Db.Migrations b.ToTable("WhiteboardMemberships"); }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .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("AspNetRoles", (string)null); + }); + + 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("AspNetRoleClaims", (string)null); + }); + + 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("AspNetUserClaims", (string)null); + }); + + 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("AspNetUserLogins", (string)null); + }); + + 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("AspNetUserRoles", (string)null); + }); + + 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("AspNetUserTokens", (string)null); + }); + modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Shape.Shape", b => { b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "Author") @@ -222,6 +396,57 @@ namespace AipsCore.Infrastructure.Persistence.Db.Migrations b.Navigation("Whiteboard"); }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("AipsCore.Infrastructure.Persistence.User.User", b => { b.Navigation("Memberships"); From a9b4a0ce879ad0b48b494b4f9769c60dc52be29b Mon Sep 17 00:00:00 2001 From: Veljko Tosic Date: Thu, 12 Feb 2026 19:53:13 +0100 Subject: [PATCH 08/11] App to use Auth --- dotnet/AipsWebApi/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/AipsWebApi/Program.cs b/dotnet/AipsWebApi/Program.cs index d1db72b..9c75565 100644 --- a/dotnet/AipsWebApi/Program.cs +++ b/dotnet/AipsWebApi/Program.cs @@ -24,6 +24,7 @@ if (app.Environment.IsDevelopment()) app.UseMiddleware(); +app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); From 86f66727f37b763c42f0655acea39f09025bf829 Mon Sep 17 00:00:00 2001 From: Veljko Tosic Date: Thu, 12 Feb 2026 19:53:58 +0100 Subject: [PATCH 09/11] Removed possibly invalid endpoints, will be refactored to use auth and user context when needed again --- .../AipsWebApi/Controllers/ShapeController.cs | 21 ---------- .../AipsWebApi/Controllers/UserController.cs | 28 ++++++------- .../Controllers/WhiteboardController.cs | 39 +------------------ 3 files changed, 17 insertions(+), 71 deletions(-) diff --git a/dotnet/AipsWebApi/Controllers/ShapeController.cs b/dotnet/AipsWebApi/Controllers/ShapeController.cs index 47e47d9..4da93cc 100644 --- a/dotnet/AipsWebApi/Controllers/ShapeController.cs +++ b/dotnet/AipsWebApi/Controllers/ShapeController.cs @@ -16,25 +16,4 @@ public class ShapeController : ControllerBase { _dispatcher = dispatcher; } - - [HttpPost("arrow")] - public async Task CreateArrow(CreateArrowCommand command, CancellationToken cancellationToken) - { - var result = await _dispatcher.Execute(command, cancellationToken); - return Ok(result); - } - - [HttpPost("textShape")] - public async Task CreateTextShape(CreateTextShapeCommand command, CancellationToken cancellationToken) - { - var result = await _dispatcher.Execute(command, cancellationToken); - return Ok(result); - } - - [HttpPost("line")] - public async Task CreateLine(CreateLineCommand command, CancellationToken cancellationToken) - { - var result = await _dispatcher.Execute(command, cancellationToken); - return Ok(result); - } } \ No newline at end of file diff --git a/dotnet/AipsWebApi/Controllers/UserController.cs b/dotnet/AipsWebApi/Controllers/UserController.cs index fe49a91..1987388 100644 --- a/dotnet/AipsWebApi/Controllers/UserController.cs +++ b/dotnet/AipsWebApi/Controllers/UserController.cs @@ -1,8 +1,9 @@ using AipsCore.Application.Abstract; -using AipsCore.Application.Models.User.Command.CreateUser; +using AipsCore.Application.Authentication; +using AipsCore.Application.Models.User.Command.LogIn; +using AipsCore.Application.Models.User.Command.SignUp; using AipsCore.Application.Models.User.Query.GetUser; -using AipsCore.Domain.Common.Validation; -using AipsCore.Domain.Models.User.ValueObjects; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace AipsWebApi.Controllers; @@ -18,18 +19,19 @@ public class UserController : ControllerBase _dispatcher = dispatcher; } - [HttpGet("{userId}")] - public async Task GetUser([FromRoute] string userId, CancellationToken cancellationToken) + [AllowAnonymous] + [HttpPost("signup")] + public async Task SignUp(SignUpUserCommand command, CancellationToken cancellationToken) { - var query = new GetUserQuery(userId); - var result = await _dispatcher.Execute(query, cancellationToken); - return Ok(result); + var result = await _dispatcher.Execute(command, cancellationToken); + return Ok(result.IdValue); } - - [HttpPost] - public async Task> CreateUser(CreateUserCommand command, CancellationToken cancellationToken) + + [AllowAnonymous] + [HttpPost("login")] + public async Task> LogIn(LogInUserCommand command, CancellationToken cancellationToken) { - var userId = await _dispatcher.Execute(command, cancellationToken); - return Ok(userId.IdValue); + var result = await _dispatcher.Execute(command, cancellationToken); + return Ok(result.Value); } } \ No newline at end of file diff --git a/dotnet/AipsWebApi/Controllers/WhiteboardController.cs b/dotnet/AipsWebApi/Controllers/WhiteboardController.cs index 3be4f40..af8cd08 100644 --- a/dotnet/AipsWebApi/Controllers/WhiteboardController.cs +++ b/dotnet/AipsWebApi/Controllers/WhiteboardController.cs @@ -6,6 +6,7 @@ using AipsCore.Application.Models.Whiteboard.Command.KickUserFromWhiteboard; using AipsCore.Application.Models.Whiteboard.Command.UnbanUserFromWhiteboard; using AipsCore.Application.Models.Whiteboard.Query.GetRecentWhiteboards; using AipsCore.Domain.Models.Whiteboard; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace AipsWebApi.Controllers; @@ -21,47 +22,11 @@ public class WhiteboardController : ControllerBase _dispatcher = dispatcher; } + [Authorize] [HttpPost] public async Task> CreateWhiteboard(CreateWhiteboardCommand command, CancellationToken cancellationToken) { var whiteboardId = await _dispatcher.Execute(command, cancellationToken); return Ok(whiteboardId.IdValue); } - - [HttpPost("adduser")] - public async Task AddUser(AddUserToWhiteboardCommand command, - CancellationToken cancellationToken) - { - await _dispatcher.Execute(command, cancellationToken); - return Ok(); - } - - [HttpGet("recent")] - public async Task>> Recent(GetRecentWhiteboardsQuery query, CancellationToken cancellationToken) - { - var result = await _dispatcher.Execute(query, cancellationToken); - - return Ok(result); - } - - [HttpPut("banUser")] - public async Task BanUserFromWhiteboard(BanUserFromWhiteboardCommand command, CancellationToken cancellationToken) - { - await _dispatcher.Execute(command, cancellationToken); - return Ok(); - } - - [HttpPut("unbanUser")] - public async Task UnbanUserFromWhiteboard(UnbanUserFromWhiteboardCommand command, CancellationToken cancellationToken) - { - await _dispatcher.Execute(command, cancellationToken); - return Ok(); - } - - [HttpPut("kickUser")] - public async Task KickUserFromWhiteboard(KickUserFromWhiteboardCommand command, CancellationToken cancellationToken) - { - await _dispatcher.Execute(command, cancellationToken); - return Ok(); - } } \ No newline at end of file From 5ecf8435f43348053a19ab64eb7a3e4480906997 Mon Sep 17 00:00:00 2001 From: Veljko Tosic Date: Thu, 12 Feb 2026 20:25:51 +0100 Subject: [PATCH 10/11] Added roles (may or may not be used) --- .../Domain/Models/User/External/UserRole.cs | 26 +++++++++++++++++ .../Models/User/Validation/UserErrors.cs | 9 ++++++ .../Persistence/Db/DbInitializer.cs | 29 +++++++++++++++++++ .../Persistence/User/UserRepository.cs | 3 +- dotnet/AipsWebApi/Program.cs | 7 ++++- 5 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 dotnet/AipsCore/Domain/Models/User/External/UserRole.cs create mode 100644 dotnet/AipsCore/Infrastructure/Persistence/Db/DbInitializer.cs diff --git a/dotnet/AipsCore/Domain/Models/User/External/UserRole.cs b/dotnet/AipsCore/Domain/Models/User/External/UserRole.cs new file mode 100644 index 0000000..de0a13e --- /dev/null +++ b/dotnet/AipsCore/Domain/Models/User/External/UserRole.cs @@ -0,0 +1,26 @@ +using AipsCore.Domain.Common.Validation; +using AipsCore.Domain.Models.User.Validation; + +namespace AipsCore.Domain.Models.User.External; + +public record UserRole +{ + public string Name { get; init; } + + private UserRole(string Name) + { + this.Name = Name; + } + + public static UserRole User => new("User"); + public static UserRole Admin => new("Admin"); + + public static IEnumerable All() => [User, Admin]; + + public static UserRole FromString(string name) + { + var role = All().FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + + return role ?? throw new ValidationException(UserErrors.RoleDoesNotExist(name)); + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs b/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs index 8f8b122..62affaa 100644 --- a/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs +++ b/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs @@ -1,5 +1,6 @@ using AipsCore.Domain.Abstract.Validation; using AipsCore.Domain.Common.Validation; +using AipsCore.Domain.Models.User.External; using AipsCore.Domain.Models.User.ValueObjects; namespace AipsCore.Domain.Models.User.Validation; @@ -21,4 +22,12 @@ public class UserErrors : AbstractErrors return CreateValidationError(code, message); } + + public static ValidationError RoleDoesNotExist(string name) + { + string code = "user_role_does_not_exist"; + string message = $"Role '{name}' does not exist"; + + return CreateValidationError(code, message); + } } \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Db/DbInitializer.cs b/dotnet/AipsCore/Infrastructure/Persistence/Db/DbInitializer.cs new file mode 100644 index 0000000..81edaf3 --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/Persistence/Db/DbInitializer.cs @@ -0,0 +1,29 @@ +using AipsCore.Domain.Models.User.External; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; + +namespace AipsCore.Infrastructure.Persistence.Db; + +public static class DbInitializer +{ + public static async Task SeedRolesAsync(IServiceProvider serviceProvider) + { + using var scope = serviceProvider.CreateScope(); + var roleManager = scope.ServiceProvider.GetRequiredService>>(); + + var roleNames = UserRole.All(); + + foreach (var roleName in roleNames) + { + var roleExist = await roleManager.RoleExistsAsync(roleName.Name); + if (!roleExist) + { + await roleManager.CreateAsync(new IdentityRole + { + Name = roleName.Name, + NormalizedName = roleName.Name.ToUpperInvariant() + }); + } + } + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/Persistence/User/UserRepository.cs b/dotnet/AipsCore/Infrastructure/Persistence/User/UserRepository.cs index e4f6965..525a36d 100644 --- a/dotnet/AipsCore/Infrastructure/Persistence/User/UserRepository.cs +++ b/dotnet/AipsCore/Infrastructure/Persistence/User/UserRepository.cs @@ -65,8 +65,7 @@ public class UserRepository : AbstractRepository LoginWithEmailAndPasswordAsync(string email, string password, CancellationToken cancellationToken = default) diff --git a/dotnet/AipsWebApi/Program.cs b/dotnet/AipsWebApi/Program.cs index 9c75565..42ebe7c 100644 --- a/dotnet/AipsWebApi/Program.cs +++ b/dotnet/AipsWebApi/Program.cs @@ -1,4 +1,5 @@ using AipsCore.Infrastructure.DI; +using AipsCore.Infrastructure.Persistence.Db; using AipsWebApi.Middleware; using DotNetEnv; @@ -15,13 +16,17 @@ builder.Services.AddAips(builder.Configuration); var app = builder.Build(); +using (var scope = app.Services.CreateScope()) +{ + await DbInitializer.SeedRolesAsync(scope.ServiceProvider); +} + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } - app.UseMiddleware(); app.UseAuthentication(); From 8253e8bc3f7612738264750876e502ae8e73d64d Mon Sep 17 00:00:00 2001 From: Veljko Tosic Date: Thu, 12 Feb 2026 22:04:48 +0100 Subject: [PATCH 11/11] Fixes --- .../Abstract/UserContext/ITokenProvider.cs | 3 +- .../Application/Authentication/Token.cs | 3 - .../Common/Authentication/IAuthService.cs | 9 +++ .../Common/Authentication/LoginResult.cs | 6 ++ .../Common/Authentication/Token.cs | 3 + .../User/Command/LogIn/LogInUserCommand.cs | 2 +- .../Command/LogIn/LogInUserCommandHandler.cs | 12 ++-- .../SignUp/SignUpUserCommandHandler.cs | 11 +-- .../Models/User/External/IUserRepository.cs | 3 +- .../Models/User/External/LoginResult.cs | 3 - .../Domain/Models/User/External/UserRole.cs | 6 +- .../User/Options/UserOptionsDefaults.Auth.cs | 12 ++++ .../Models/User/Validation/UserErrors.cs | 14 +--- .../ConfigurationEnvExtensions.cs | 67 ++++++++++++++++--- .../Infrastructure/DI/StartupExtensions.cs | 16 +++++ .../DI/UserContextRegistrationExtension.cs | 24 ++++--- .../Authentication/EfAuthService.cs | 64 ++++++++++++++++++ .../Authentication/JwtTokenProvider.cs | 5 +- .../Persistence/User/Mappers/UserMappers.cs | 39 +++++++++++ .../Persistence/User/UserRepository.cs | 67 ++----------------- .../AipsWebApi/Controllers/UserController.cs | 2 +- dotnet/AipsWebApi/Program.cs | 5 +- 22 files changed, 250 insertions(+), 126 deletions(-) delete mode 100644 dotnet/AipsCore/Application/Authentication/Token.cs create mode 100644 dotnet/AipsCore/Application/Common/Authentication/IAuthService.cs create mode 100644 dotnet/AipsCore/Application/Common/Authentication/LoginResult.cs create mode 100644 dotnet/AipsCore/Application/Common/Authentication/Token.cs delete mode 100644 dotnet/AipsCore/Domain/Models/User/External/LoginResult.cs create mode 100644 dotnet/AipsCore/Domain/Models/User/Options/UserOptionsDefaults.Auth.cs create mode 100644 dotnet/AipsCore/Infrastructure/DI/StartupExtensions.cs create mode 100644 dotnet/AipsCore/Infrastructure/Persistence/Authentication/EfAuthService.cs create mode 100644 dotnet/AipsCore/Infrastructure/Persistence/User/Mappers/UserMappers.cs diff --git a/dotnet/AipsCore/Application/Abstract/UserContext/ITokenProvider.cs b/dotnet/AipsCore/Application/Abstract/UserContext/ITokenProvider.cs index 7839f5f..26932bf 100644 --- a/dotnet/AipsCore/Application/Abstract/UserContext/ITokenProvider.cs +++ b/dotnet/AipsCore/Application/Abstract/UserContext/ITokenProvider.cs @@ -1,8 +1,9 @@ using AipsCore.Domain.Models.User; +using AipsCore.Domain.Models.User.External; namespace AipsCore.Application.Abstract.UserContext; public interface ITokenProvider { - string Generate(User user, IList roles); + string Generate(User user, IList roles); } \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Authentication/Token.cs b/dotnet/AipsCore/Application/Authentication/Token.cs deleted file mode 100644 index 85915f3..0000000 --- a/dotnet/AipsCore/Application/Authentication/Token.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace AipsCore.Application.Authentication; - -public record Token(string Value); \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Common/Authentication/IAuthService.cs b/dotnet/AipsCore/Application/Common/Authentication/IAuthService.cs new file mode 100644 index 0000000..f5c3215 --- /dev/null +++ b/dotnet/AipsCore/Application/Common/Authentication/IAuthService.cs @@ -0,0 +1,9 @@ +using AipsCore.Domain.Models.User; + +namespace AipsCore.Application.Common.Authentication; + +public interface IAuthService +{ + Task SignUpWithPasswordAsync(User user, string password, CancellationToken cancellationToken = default); + Task LoginWithEmailAndPasswordAsync(string email, string password, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Common/Authentication/LoginResult.cs b/dotnet/AipsCore/Application/Common/Authentication/LoginResult.cs new file mode 100644 index 0000000..16e22c4 --- /dev/null +++ b/dotnet/AipsCore/Application/Common/Authentication/LoginResult.cs @@ -0,0 +1,6 @@ +using AipsCore.Domain.Models.User; +using AipsCore.Domain.Models.User.External; + +namespace AipsCore.Application.Common.Authentication; + +public record LoginResult(User User, IList Roles); \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Common/Authentication/Token.cs b/dotnet/AipsCore/Application/Common/Authentication/Token.cs new file mode 100644 index 0000000..18d5af1 --- /dev/null +++ b/dotnet/AipsCore/Application/Common/Authentication/Token.cs @@ -0,0 +1,3 @@ +namespace AipsCore.Application.Common.Authentication; + +public record Token(string Value); \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommand.cs b/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommand.cs index 3a2312d..216eb8c 100644 --- a/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommand.cs +++ b/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommand.cs @@ -1,5 +1,5 @@ using AipsCore.Application.Abstract.Command; -using AipsCore.Application.Authentication; +using AipsCore.Application.Common.Authentication; namespace AipsCore.Application.Models.User.Command.LogIn; diff --git a/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommandHandler.cs b/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommandHandler.cs index 2316407..eebaa7d 100644 --- a/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommandHandler.cs +++ b/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommandHandler.cs @@ -1,6 +1,6 @@ using AipsCore.Application.Abstract.Command; using AipsCore.Application.Abstract.UserContext; -using AipsCore.Application.Authentication; +using AipsCore.Application.Common.Authentication; using AipsCore.Domain.Abstract; using AipsCore.Domain.Models.User.External; @@ -10,18 +10,18 @@ public class LogInUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; private readonly ITokenProvider _tokenProvider; - private readonly IUnitOfWork _unitOfWork; - - public LogInUserCommandHandler(IUserRepository userRepository, ITokenProvider tokenProvider, IUnitOfWork unitOfWork) + private readonly IAuthService _authService; + + public LogInUserCommandHandler(IUserRepository userRepository, ITokenProvider tokenProvider, IAuthService authService) { _userRepository = userRepository; _tokenProvider = tokenProvider; - _unitOfWork = unitOfWork; + _authService = authService; } public async Task Handle(LogInUserCommand command, CancellationToken cancellationToken = default) { - var loginResult = await _userRepository.LoginWithEmailAndPasswordAsync(command.Email, command.Password, cancellationToken); + var loginResult = await _authService.LoginWithEmailAndPasswordAsync(command.Email, command.Password, cancellationToken); return new Token(_tokenProvider.Generate(loginResult.User, loginResult.Roles)); } diff --git a/dotnet/AipsCore/Application/Models/User/Command/SignUp/SignUpUserCommandHandler.cs b/dotnet/AipsCore/Application/Models/User/Command/SignUp/SignUpUserCommandHandler.cs index 722d811..925c22f 100644 --- a/dotnet/AipsCore/Application/Models/User/Command/SignUp/SignUpUserCommandHandler.cs +++ b/dotnet/AipsCore/Application/Models/User/Command/SignUp/SignUpUserCommandHandler.cs @@ -1,4 +1,5 @@ using AipsCore.Application.Abstract.Command; +using AipsCore.Application.Common.Authentication; using AipsCore.Domain.Abstract; using AipsCore.Domain.Models.User.External; using AipsCore.Domain.Models.User.ValueObjects; @@ -8,19 +9,19 @@ namespace AipsCore.Application.Models.User.Command.SignUp; public class SignUpUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; - private readonly IUnitOfWork _unitOfWork; - - public SignUpUserCommandHandler(IUserRepository userRepository, IUnitOfWork unitOfWork) + private readonly IAuthService _authService; + + public SignUpUserCommandHandler(IUserRepository userRepository, IAuthService authService) { _userRepository = userRepository; - _unitOfWork = unitOfWork; + _authService = authService; } public async Task Handle(SignUpUserCommand command, CancellationToken cancellationToken = default) { var user = Domain.Models.User.User.Create(command.Email, command.Username); - await _userRepository.SignUpWithPasswordAsync(user, command.Password, cancellationToken); + await _authService.SignUpWithPasswordAsync(user, command.Password, cancellationToken); return user.Id; } diff --git a/dotnet/AipsCore/Domain/Models/User/External/IUserRepository.cs b/dotnet/AipsCore/Domain/Models/User/External/IUserRepository.cs index edc3107..7a9f154 100644 --- a/dotnet/AipsCore/Domain/Models/User/External/IUserRepository.cs +++ b/dotnet/AipsCore/Domain/Models/User/External/IUserRepository.cs @@ -5,6 +5,5 @@ namespace AipsCore.Domain.Models.User.External; public interface IUserRepository : IAbstractRepository { - Task SignUpWithPasswordAsync(User user, string password, CancellationToken cancellationToken = default); - Task LoginWithEmailAndPasswordAsync(string email, string password, CancellationToken cancellationToken = default); + } \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/User/External/LoginResult.cs b/dotnet/AipsCore/Domain/Models/User/External/LoginResult.cs deleted file mode 100644 index 3988a20..0000000 --- a/dotnet/AipsCore/Domain/Models/User/External/LoginResult.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace AipsCore.Domain.Models.User.External; - -public record LoginResult(User User, IList Roles); \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/User/External/UserRole.cs b/dotnet/AipsCore/Domain/Models/User/External/UserRole.cs index de0a13e..5712d3d 100644 --- a/dotnet/AipsCore/Domain/Models/User/External/UserRole.cs +++ b/dotnet/AipsCore/Domain/Models/User/External/UserRole.cs @@ -17,10 +17,8 @@ public record UserRole public static IEnumerable All() => [User, Admin]; - public static UserRole FromString(string name) + public static UserRole? FromString(string name) { - var role = All().FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - - return role ?? throw new ValidationException(UserErrors.RoleDoesNotExist(name)); + return All().FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); } } \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/User/Options/UserOptionsDefaults.Auth.cs b/dotnet/AipsCore/Domain/Models/User/Options/UserOptionsDefaults.Auth.cs new file mode 100644 index 0000000..959ce5e --- /dev/null +++ b/dotnet/AipsCore/Domain/Models/User/Options/UserOptionsDefaults.Auth.cs @@ -0,0 +1,12 @@ +namespace AipsCore.Domain.Models.User.Options; + +public static partial class UserOptionsDefaults +{ + public const int PasswordRequiredLength = 8; + public const bool PasswordRequireDigit = true; + public const bool PasswordRequireLowercase = true; + public const bool PasswordRequireUppercase = true; + public const bool PasswordRequireNonAlphanumeric = true; + + public const bool UserRequireUniqueEmail = true; +} \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs b/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs index 62affaa..7675aa1 100644 --- a/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs +++ b/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs @@ -7,18 +7,10 @@ namespace AipsCore.Domain.Models.User.Validation; public class UserErrors : AbstractErrors { - public static ValidationError LoginErrorUserNotFoundByEmail(string email) + public static ValidationError InvalidCredentials() { - string code = "login_error_user_not_found_by_email"; - string message = $"User with email '{email}' not found"; - - return CreateValidationError(code, message); - } - - public static ValidationError LoginErrorIncorrectPassword() - { - string code = "login_error_incorrect_password"; - string message = $"Incorrect password provided"; + string code = "invalid_credentials"; + string message = "Invalid credentials"; return CreateValidationError(code, message); } diff --git a/dotnet/AipsCore/Infrastructure/DI/Configuration/ConfigurationEnvExtensions.cs b/dotnet/AipsCore/Infrastructure/DI/Configuration/ConfigurationEnvExtensions.cs index 69d8ec1..8c56742 100644 --- a/dotnet/AipsCore/Infrastructure/DI/Configuration/ConfigurationEnvExtensions.cs +++ b/dotnet/AipsCore/Infrastructure/DI/Configuration/ConfigurationEnvExtensions.cs @@ -5,21 +5,66 @@ namespace AipsCore.Infrastructure.DI.Configuration; public static class ConfigurationEnvExtensions { private const string DbConnStringKey = "DB_CONN_STRING"; + + private const string JwtIssuer = "JWT_ISSUER"; + private const string JwtAudience = "JWT_AUDIENCE"; + private const string JwtKey = "JWT_KEY"; + private const string JwtExpirationMinutes = "JWT_EXPIRATION_MINUTES"; - public static string GetEnvConnectionString(this IConfiguration configuration) + extension(IConfiguration configuration) { - return configuration.GetEnvForSure(DbConnStringKey); - } - - private static string GetEnvForSure(this IConfiguration configuration, string key) - { - var value = configuration[key]; - - if (value is null) + public string GetEnvConnectionString() { - throw new ConfigurationException(key); + return configuration.GetEnvForSure(DbConnStringKey); + } + + public string GetEnvJwtIssuer() + { + return configuration.GetEnvForSure(JwtIssuer); + } + + public string GetEnvJwtAudience() + { + return configuration.GetEnvForSure(JwtAudience); + } + + public string GetEnvJwtKey() + { + return configuration.GetEnvForSure(JwtKey); + } + + public int GetEnvJwtExpirationMinutes() + { + return configuration.GetEnvInt(configuration.GetEnvOrDefault(JwtExpirationMinutes, "60")); + } + + private string GetEnvForSure(string key) + { + var value = configuration[key]; + + if (value is null) + { + throw new ConfigurationException(key); + } + + return value; } - return value; + private string GetEnvOrDefault(string key, string defaultValue) + { + return configuration.GetValue(key, defaultValue); + } + + private int GetEnvInt(string value) + { + if (int.TryParse(value, out var result)) + { + return result; + } + else + { + throw new ConfigurationException($"Value '{value}' is not a valid integer."); + } + } } } \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/DI/StartupExtensions.cs b/dotnet/AipsCore/Infrastructure/DI/StartupExtensions.cs new file mode 100644 index 0000000..0a7265e --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/DI/StartupExtensions.cs @@ -0,0 +1,16 @@ +using AipsCore.Infrastructure.Persistence.Db; +using Microsoft.Extensions.DependencyInjection; + +namespace AipsCore.Infrastructure.DI; + +public static class StartupExtensions +{ + public static async Task InitializeInfrastructureAsync(this IServiceProvider services) + { + using var scope = services.CreateScope(); + + var serviceProvider = scope.ServiceProvider; + + await DbInitializer.SeedRolesAsync(serviceProvider); + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/DI/UserContextRegistrationExtension.cs b/dotnet/AipsCore/Infrastructure/DI/UserContextRegistrationExtension.cs index f051b43..e3e5ea1 100644 --- a/dotnet/AipsCore/Infrastructure/DI/UserContextRegistrationExtension.cs +++ b/dotnet/AipsCore/Infrastructure/DI/UserContextRegistrationExtension.cs @@ -1,5 +1,8 @@ using System.Text; using AipsCore.Application.Abstract.UserContext; +using AipsCore.Application.Common.Authentication; +using AipsCore.Domain.Models.User.Options; +using AipsCore.Infrastructure.DI.Configuration; using AipsCore.Infrastructure.Persistence.Authentication; using AipsCore.Infrastructure.Persistence.Db; using AipsCore.Infrastructure.Persistence.User; @@ -17,10 +20,10 @@ public static class UserContextRegistrationExtension { var jwtSettings = new JwtSettings { - Issuer = configuration["JWT_ISSUER"]!, - Audience = configuration["JWT_AUDIENCE"]!, - Key = configuration["JWT_KEY"]!, - ExpirationMinutes = int.Parse(configuration["JWT_EXPIRATION_MINUTES"] ?? "60") + Issuer = configuration.GetEnvJwtIssuer(), + Audience = configuration.GetEnvJwtAudience(), + Key = configuration.GetEnvJwtKey(), + ExpirationMinutes = configuration.GetEnvJwtExpirationMinutes() }; services.AddSingleton(jwtSettings); @@ -29,13 +32,13 @@ public static class UserContextRegistrationExtension services.AddIdentityCore(options => { - options.Password.RequiredLength = 8; - options.Password.RequireDigit = true; - options.Password.RequireLowercase = true; - options.Password.RequireUppercase = true; - options.Password.RequireNonAlphanumeric = true; + options.Password.RequiredLength = UserOptionsDefaults.PasswordRequiredLength; + options.Password.RequireDigit = UserOptionsDefaults.PasswordRequireDigit; + options.Password.RequireLowercase = UserOptionsDefaults.PasswordRequireLowercase; + options.Password.RequireUppercase = UserOptionsDefaults.PasswordRequireUppercase; + options.Password.RequireNonAlphanumeric = UserOptionsDefaults.PasswordRequireNonAlphanumeric; - options.User.RequireUniqueEmail = true; + options.User.RequireUniqueEmail = UserOptionsDefaults.UserRequireUniqueEmail; }) .AddRoles>() .AddEntityFrameworkStores() @@ -62,6 +65,7 @@ public static class UserContextRegistrationExtension services.AddTransient(); services.AddTransient(); + services.AddTransient(); return services; } diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Authentication/EfAuthService.cs b/dotnet/AipsCore/Infrastructure/Persistence/Authentication/EfAuthService.cs new file mode 100644 index 0000000..700bf42 --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/Persistence/Authentication/EfAuthService.cs @@ -0,0 +1,64 @@ +using AipsCore.Application.Common.Authentication; +using AipsCore.Domain.Common.Validation; +using AipsCore.Domain.Models.User.External; +using AipsCore.Domain.Models.User.Validation; +using AipsCore.Infrastructure.Persistence.User.Mappers; +using Microsoft.AspNetCore.Identity; + +namespace AipsCore.Infrastructure.Persistence.Authentication; + +public class EfAuthService : IAuthService +{ + private readonly UserManager _userManager; + + public EfAuthService(UserManager userManager) + { + _userManager = userManager; + } + + public async Task SignUpWithPasswordAsync(Domain.Models.User.User user, string password, CancellationToken cancellationToken = default) + { + var entity = user.MapToEntity(); + + var result = await _userManager.CreateAsync(entity, password); + + if (!result.Succeeded) + { + var errors = string.Join(", ", result.Errors.Select(e => e.Description)); + throw new Exception($"User registration failed: {errors}"); + } + + await _userManager.AddToRoleAsync(entity, UserRole.User.Name); + } + + public async Task LoginWithEmailAndPasswordAsync(string email, string password, CancellationToken cancellationToken = default) + { + var entity = await _userManager.FindByEmailAsync(email); + + if (entity is null) + { + throw new ValidationException(UserErrors.InvalidCredentials()); + } + + var isPasswordValid = await _userManager.CheckPasswordAsync(entity, password); + + if (!isPasswordValid) + { + throw new ValidationException(UserErrors.InvalidCredentials()); + } + + var roles = new List(); + var rolesNames = await _userManager.GetRolesAsync(entity); + + foreach (var roleName in rolesNames) + { + var role = UserRole.FromString(roleName); + + if (role is null) throw new Exception($"Role {roleName} not found"); + + roles.Add(role); + } + + return new LoginResult(entity.MapToModel(), roles); + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Authentication/JwtTokenProvider.cs b/dotnet/AipsCore/Infrastructure/Persistence/Authentication/JwtTokenProvider.cs index c80883b..2ccc79e 100644 --- a/dotnet/AipsCore/Infrastructure/Persistence/Authentication/JwtTokenProvider.cs +++ b/dotnet/AipsCore/Infrastructure/Persistence/Authentication/JwtTokenProvider.cs @@ -2,6 +2,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using AipsCore.Application.Abstract.UserContext; +using AipsCore.Domain.Models.User.External; using Microsoft.IdentityModel.Tokens; namespace AipsCore.Infrastructure.Persistence.Authentication; @@ -15,7 +16,7 @@ public class JwtTokenProvider : ITokenProvider _jwtSettings = jwtSettings; } - public string Generate(Domain.Models.User.User user, IList roles) + public string Generate(Domain.Models.User.User user, IList roles) { var claims = new List { @@ -25,7 +26,7 @@ public class JwtTokenProvider : ITokenProvider foreach (var role in roles) { - claims.Add(new Claim(ClaimTypes.Role, role)); + claims.Add(new Claim(ClaimTypes.Role, role.Name)); } var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Key)); diff --git a/dotnet/AipsCore/Infrastructure/Persistence/User/Mappers/UserMappers.cs b/dotnet/AipsCore/Infrastructure/Persistence/User/Mappers/UserMappers.cs new file mode 100644 index 0000000..8cfe7b4 --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/Persistence/User/Mappers/UserMappers.cs @@ -0,0 +1,39 @@ +namespace AipsCore.Infrastructure.Persistence.User.Mappers; + +public static class UserMappers +{ + public static Domain.Models.User.User MapToModel(this User entity) + { + return Domain.Models.User.User.Create( + entity.Id.ToString(), + entity.Email!, + entity.UserName!, + entity.CreatedAt, + entity.DeletedAt + ); + } + + public static User MapToEntity(this Domain.Models.User.User model) + { + return new User + { + Id = new Guid(model.Id.IdValue), + Email = model.Email.EmailValue, + NormalizedEmail = model.Email.EmailValue.ToUpperInvariant(), + UserName = model.Username.UsernameValue, + NormalizedUserName = model.Username.UsernameValue.ToUpperInvariant(), + CreatedAt = model.CreatedAt.CreatedAtValue, + DeletedAt = model.DeletedAt.DeletedAtValue + }; + } + + public static void UpdateEntity(this User entity, Domain.Models.User.User model) + { + entity.Email = model.Email.EmailValue; + entity.NormalizedEmail = model.Email.EmailValue.ToUpperInvariant(); + entity.UserName = model.Username.UsernameValue; + entity.NormalizedUserName = model.Username.UsernameValue.ToUpperInvariant(); + entity.CreatedAt = model.CreatedAt.CreatedAtValue; + entity.DeletedAt = model.DeletedAt.DeletedAtValue; + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/Persistence/User/UserRepository.cs b/dotnet/AipsCore/Infrastructure/Persistence/User/UserRepository.cs index 525a36d..b650917 100644 --- a/dotnet/AipsCore/Infrastructure/Persistence/User/UserRepository.cs +++ b/dotnet/AipsCore/Infrastructure/Persistence/User/UserRepository.cs @@ -4,88 +4,31 @@ using AipsCore.Domain.Models.User.Validation; using AipsCore.Domain.Models.User.ValueObjects; using AipsCore.Infrastructure.Persistence.Abstract; using AipsCore.Infrastructure.Persistence.Db; +using AipsCore.Infrastructure.Persistence.User.Mappers; using Microsoft.AspNetCore.Identity; namespace AipsCore.Infrastructure.Persistence.User; public class UserRepository : AbstractRepository, IUserRepository { - private readonly UserManager _userManager; - public UserRepository(AipsDbContext context, UserManager userManager) : base(context) { - _userManager = userManager; + } protected override Domain.Models.User.User MapToModel(User entity) { - return Domain.Models.User.User.Create( - entity.Id.ToString(), - entity.Email, - entity.UserName, - entity.CreatedAt, - entity.DeletedAt - ); + return entity.MapToModel(); } protected override User MapToEntity(Domain.Models.User.User model) { - return new User - { - Id = new Guid(model.Id.IdValue), - Email = model.Email.EmailValue, - NormalizedEmail = model.Email.EmailValue.ToUpperInvariant(), - UserName = model.Username.UsernameValue, - NormalizedUserName = model.Username.UsernameValue.ToUpperInvariant(), - CreatedAt = model.CreatedAt.CreatedAtValue, - DeletedAt = model.DeletedAt.DeletedAtValue - }; + return model.MapToEntity(); } protected override void UpdateEntity(User entity, Domain.Models.User.User model) { - entity.Email = model.Email.EmailValue; - entity.NormalizedEmail = model.Email.EmailValue.ToUpperInvariant(); - entity.UserName = model.Username.UsernameValue; - entity.NormalizedUserName = model.Username.UsernameValue.ToUpperInvariant(); - entity.CreatedAt = model.CreatedAt.CreatedAtValue; - entity.DeletedAt = model.DeletedAt.DeletedAtValue; - } - - public async Task SignUpWithPasswordAsync(Domain.Models.User.User user, string password, CancellationToken cancellationToken = default) - { - var entity = MapToEntity(user); - - var result = await _userManager.CreateAsync(entity, password); - - if (!result.Succeeded) - { - var errors = string.Join(", ", result.Errors.Select(e => e.Description)); - throw new Exception($"User registration failed: {errors}"); - } - - await _userManager.AddToRoleAsync(entity, UserRole.User.Name); - } - - public async Task LoginWithEmailAndPasswordAsync(string email, string password, CancellationToken cancellationToken = default) - { - var entity = await _userManager.FindByEmailAsync(email); - - if (entity is null) - { - throw new ValidationException(UserErrors.LoginErrorUserNotFoundByEmail(email)); - } - - var isPasswordValid = await _userManager.CheckPasswordAsync(entity, password); - - if (!isPasswordValid) - { - throw new ValidationException(UserErrors.LoginErrorIncorrectPassword()); - } - - var roles = await _userManager.GetRolesAsync(entity); - - return new LoginResult(MapToModel(entity), roles); + entity.UpdateEntity(model); } } \ No newline at end of file diff --git a/dotnet/AipsWebApi/Controllers/UserController.cs b/dotnet/AipsWebApi/Controllers/UserController.cs index 1987388..746887f 100644 --- a/dotnet/AipsWebApi/Controllers/UserController.cs +++ b/dotnet/AipsWebApi/Controllers/UserController.cs @@ -1,5 +1,5 @@ using AipsCore.Application.Abstract; -using AipsCore.Application.Authentication; +using AipsCore.Application.Common.Authentication; using AipsCore.Application.Models.User.Command.LogIn; using AipsCore.Application.Models.User.Command.SignUp; using AipsCore.Application.Models.User.Query.GetUser; diff --git a/dotnet/AipsWebApi/Program.cs b/dotnet/AipsWebApi/Program.cs index 42ebe7c..3629922 100644 --- a/dotnet/AipsWebApi/Program.cs +++ b/dotnet/AipsWebApi/Program.cs @@ -16,10 +16,7 @@ builder.Services.AddAips(builder.Configuration); var app = builder.Build(); -using (var scope = app.Services.CreateScope()) -{ - await DbInitializer.SeedRolesAsync(scope.ServiceProvider); -} +await app.Services.InitializeInfrastructureAsync(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment())