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 diff --git a/dotnet/AipsCore/Application/Abstract/UserContext/ITokenProvider.cs b/dotnet/AipsCore/Application/Abstract/UserContext/ITokenProvider.cs new file mode 100644 index 0000000..26932bf --- /dev/null +++ b/dotnet/AipsCore/Application/Abstract/UserContext/ITokenProvider.cs @@ -0,0 +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); +} \ 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 new file mode 100644 index 0000000..216eb8c --- /dev/null +++ b/dotnet/AipsCore/Application/Models/User/Command/LogIn/LogInUserCommand.cs @@ -0,0 +1,6 @@ +using AipsCore.Application.Abstract.Command; +using AipsCore.Application.Common.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..eebaa7d --- /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.Common.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 IAuthService _authService; + + public LogInUserCommandHandler(IUserRepository userRepository, ITokenProvider tokenProvider, IAuthService authService) + { + _userRepository = userRepository; + _tokenProvider = tokenProvider; + _authService = authService; + } + + public async Task Handle(LogInUserCommand command, CancellationToken cancellationToken = default) + { + var loginResult = await _authService.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..925c22f --- /dev/null +++ b/dotnet/AipsCore/Application/Models/User/Command/SignUp/SignUpUserCommandHandler.cs @@ -0,0 +1,28 @@ +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; + +namespace AipsCore.Application.Models.User.Command.SignUp; + +public class SignUpUserCommandHandler : ICommandHandler +{ + private readonly IUserRepository _userRepository; + private readonly IAuthService _authService; + + public SignUpUserCommandHandler(IUserRepository userRepository, IAuthService authService) + { + _userRepository = userRepository; + _authService = authService; + } + + public async Task Handle(SignUpUserCommand command, CancellationToken cancellationToken = default) + { + var user = Domain.Models.User.User.Create(command.Email, command.Username); + + await _authService.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, 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..5712d3d --- /dev/null +++ b/dotnet/AipsCore/Domain/Models/User/External/UserRole.cs @@ -0,0 +1,24 @@ +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) + { + 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 558d7a6..7675aa1 100644 --- a/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs +++ b/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs @@ -1,9 +1,25 @@ 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; public class UserErrors : AbstractErrors { + public static ValidationError InvalidCredentials() + { + string code = "invalid_credentials"; + string message = "Invalid credentials"; + + 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/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/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 7513690..e3e5ea1 100644 --- a/dotnet/AipsCore/Infrastructure/DI/UserContextRegistrationExtension.cs +++ b/dotnet/AipsCore/Infrastructure/DI/UserContextRegistrationExtension.cs @@ -1,13 +1,71 @@ +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; +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.GetEnvJwtIssuer(), + Audience = configuration.GetEnvJwtAudience(), + Key = configuration.GetEnvJwtKey(), + ExpirationMinutes = configuration.GetEnvJwtExpirationMinutes() + }; + + services.AddSingleton(jwtSettings); + + services.AddHttpContextAccessor(); + + services.AddIdentityCore(options => + { + 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 = UserOptionsDefaults.UserRequireUniqueEmail; + }) + .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(); + 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/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..2ccc79e --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/Persistence/Authentication/JwtTokenProvider.cs @@ -0,0 +1,45 @@ +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; + +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.Name)); + } + + 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/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/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"); 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/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..b650917 100644 --- a/dotnet/AipsCore/Infrastructure/Persistence/User/UserRepository.cs +++ b/dotnet/AipsCore/Infrastructure/Persistence/User/UserRepository.cs @@ -1,45 +1,34 @@ +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 AipsCore.Infrastructure.Persistence.User.Mappers; +using Microsoft.AspNetCore.Identity; namespace AipsCore.Infrastructure.Persistence.User; public class UserRepository : AbstractRepository, IUserRepository { - public UserRepository(AipsDbContext context) + public UserRepository(AipsDbContext context, UserManager userManager) : base(context) { + } 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, - Username = model.Username.UsernameValue, - 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.Username = model.Username.UsernameValue; - entity.CreatedAt = model.CreatedAt.CreatedAtValue; - entity.DeletedAt = model.DeletedAt.DeletedAtValue; + entity.UpdateEntity(model); } } \ No newline at end of file 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 diff --git a/dotnet/AipsWebApi/Controllers/ShapeController.cs b/dotnet/AipsWebApi/Controllers/ShapeController.cs index 7eaaf86..3c85af8 100644 --- a/dotnet/AipsWebApi/Controllers/ShapeController.cs +++ b/dotnet/AipsWebApi/Controllers/ShapeController.cs @@ -17,32 +17,4 @@ public class ShapeController : ControllerBase { _dispatcher = dispatcher; } - - [HttpPost("rectangle")] - public async Task> CreateRectangle(CreateRectangleCommand command, CancellationToken token) - { - var result = await _dispatcher.Execute(command, token); - return Ok(result); - } - - [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..746887f 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.Common.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 diff --git a/dotnet/AipsWebApi/Program.cs b/dotnet/AipsWebApi/Program.cs index d1db72b..3629922 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,15 +16,17 @@ builder.Services.AddAips(builder.Configuration); var app = builder.Build(); +await app.Services.InitializeInfrastructureAsync(); + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } - app.UseMiddleware(); +app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();