From e25e467af075f9596f641d541bb6d64b362c4769 Mon Sep 17 00:00:00 2001 From: EloToJa Date: Tue, 30 Jan 2024 02:22:13 +0100 Subject: [PATCH 1/8] Add Permission claims --- .../Controllers/V1/QuizController.cs | 7 ++-- .../Commands/Login/LoginCommandHandler.cs | 4 +-- .../RefreshTokenCommandHandler.cs | 4 +-- ...TokenGenerator.cs => IJwtTokenProvider.cs} | 2 +- .../Authentication/CustomClaims.cs | 6 ++++ .../Authentication/HasPermissionAttribute.cs | 33 +++++++++++++++++++ ...tTokenGenerator.cs => JwtTokenProvider.cs} | 16 +++++---- .../Authentication/Permission.cs | 6 ++++ .../DependencyInjection.cs | 2 +- 9 files changed, 65 insertions(+), 15 deletions(-) rename src/Quizer.Application/Common/Interfaces/Authentication/{IJwtTokenGenerator.cs => IJwtTokenProvider.cs} (89%) create mode 100644 src/Quizer.Infrastructure/Authentication/CustomClaims.cs create mode 100644 src/Quizer.Infrastructure/Authentication/HasPermissionAttribute.cs rename src/Quizer.Infrastructure/Authentication/{JwtTokenGenerator.cs => JwtTokenProvider.cs} (88%) create mode 100644 src/Quizer.Infrastructure/Authentication/Permission.cs diff --git a/src/Quizer.Api/Controllers/V1/QuizController.cs b/src/Quizer.Api/Controllers/V1/QuizController.cs index 21346ac..0413b98 100644 --- a/src/Quizer.Api/Controllers/V1/QuizController.cs +++ b/src/Quizer.Api/Controllers/V1/QuizController.cs @@ -12,6 +12,7 @@ using Quizer.Application.Quizes.Queries.GetQuizImage; using Quizer.Contracts.Common; using Quizer.Contracts.Quiz; +using Quizer.Infrastructure.Authentication; using System.Security.Claims; namespace Quizer.Api.Controllers.V1; @@ -29,6 +30,7 @@ public QuizController(ISender mediator, IMapper mapper) _mapper = mapper; } + [AllowAnonymous] [HttpGet] public async Task GetQuizes( string? searchTerm, @@ -53,8 +55,8 @@ public async Task GetQuizes( Problem); } - [HttpGet("{quizId:guid}")] [AllowAnonymous] + [HttpGet("{quizId:guid}")] public async Task GetQuizById(Guid quizId) { var query = new GetQuizByIdQuery(quizId); @@ -65,8 +67,8 @@ public async Task GetQuizById(Guid quizId) Problem); } - [HttpGet("{userName}/{quizSlug}")] [AllowAnonymous] + [HttpGet("{userName}/{quizSlug}")] public async Task GetQuizByName(string userName, string quizSlug) { var query = new GetQuizByNameQuery(userName, quizSlug); @@ -77,6 +79,7 @@ public async Task GetQuizByName(string userName, string quizSlug) Problem); } + [HasPermission(Permission.CreateQuiz)] [HttpPost] public async Task CreateQuiz( CreateQuizRequest request) diff --git a/src/Quizer.Application/Authentication/Commands/Login/LoginCommandHandler.cs b/src/Quizer.Application/Authentication/Commands/Login/LoginCommandHandler.cs index 25c46b1..494ae89 100644 --- a/src/Quizer.Application/Authentication/Commands/Login/LoginCommandHandler.cs +++ b/src/Quizer.Application/Authentication/Commands/Login/LoginCommandHandler.cs @@ -10,12 +10,12 @@ namespace Quizer.Application.Authentication.Commands.Login; public class LoginCommandHandler : IRequestHandler> { - private readonly IJwtTokenGenerator _jwtTokenGenerator; + private readonly IJwtTokenProvider _jwtTokenGenerator; private readonly IRefreshTokenRepository _refreshTokenRepository; private readonly UserManager _userManager; private readonly SignInManager _signInManager; - public LoginCommandHandler(IJwtTokenGenerator jwtTokenGenerator, UserManager userManager, SignInManager signInManager, IRefreshTokenRepository refreshTokenRepository) + public LoginCommandHandler(IJwtTokenProvider jwtTokenGenerator, UserManager userManager, SignInManager signInManager, IRefreshTokenRepository refreshTokenRepository) { _jwtTokenGenerator = jwtTokenGenerator; _userManager = userManager; diff --git a/src/Quizer.Application/Authentication/Commands/RefreshToken/RefreshTokenCommandHandler.cs b/src/Quizer.Application/Authentication/Commands/RefreshToken/RefreshTokenCommandHandler.cs index 8252517..670ac11 100644 --- a/src/Quizer.Application/Authentication/Commands/RefreshToken/RefreshTokenCommandHandler.cs +++ b/src/Quizer.Application/Authentication/Commands/RefreshToken/RefreshTokenCommandHandler.cs @@ -11,10 +11,10 @@ namespace Quizer.Application.Authentication.Commands.RefreshToken; public class RefreshTokenCommandHandler : IRequestHandler> { private readonly ILogger _logger; - private readonly IJwtTokenGenerator _jwtTokenGenerator; + private readonly IJwtTokenProvider _jwtTokenGenerator; private readonly IRefreshTokenRepository _refreshTokenRepository; - public RefreshTokenCommandHandler(ILogger logger, IJwtTokenGenerator jwtTokenGenerator, IRefreshTokenRepository refreshTokenRepository) + public RefreshTokenCommandHandler(ILogger logger, IJwtTokenProvider jwtTokenGenerator, IRefreshTokenRepository refreshTokenRepository) { _logger = logger; _jwtTokenGenerator = jwtTokenGenerator; diff --git a/src/Quizer.Application/Common/Interfaces/Authentication/IJwtTokenGenerator.cs b/src/Quizer.Application/Common/Interfaces/Authentication/IJwtTokenProvider.cs similarity index 89% rename from src/Quizer.Application/Common/Interfaces/Authentication/IJwtTokenGenerator.cs rename to src/Quizer.Application/Common/Interfaces/Authentication/IJwtTokenProvider.cs index f6bc69b..d238a6f 100644 --- a/src/Quizer.Application/Common/Interfaces/Authentication/IJwtTokenGenerator.cs +++ b/src/Quizer.Application/Common/Interfaces/Authentication/IJwtTokenProvider.cs @@ -2,7 +2,7 @@ namespace Quizer.Application.Common.Interfaces.Authentication; -public interface IJwtTokenGenerator +public interface IJwtTokenProvider { string GenerateAccessToken(User user); string GenerateRefreshToken(User user); diff --git a/src/Quizer.Infrastructure/Authentication/CustomClaims.cs b/src/Quizer.Infrastructure/Authentication/CustomClaims.cs new file mode 100644 index 0000000..6f0e7de --- /dev/null +++ b/src/Quizer.Infrastructure/Authentication/CustomClaims.cs @@ -0,0 +1,6 @@ +namespace Quizer.Infrastructure.Authentication; + +public static class CustomClaims +{ + public const string Permission = "permissions"; +} diff --git a/src/Quizer.Infrastructure/Authentication/HasPermissionAttribute.cs b/src/Quizer.Infrastructure/Authentication/HasPermissionAttribute.cs new file mode 100644 index 0000000..dd89a1d --- /dev/null +++ b/src/Quizer.Infrastructure/Authentication/HasPermissionAttribute.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Quizer.Infrastructure.Authentication; + +public sealed class HasPermissionAttribute : AuthorizeAttribute, IAuthorizationFilter +{ + private readonly Permission _permission; + + public HasPermissionAttribute(Permission permission) + { + _permission = permission; + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + // Check if the user is authenticated + if (context.HttpContext.User.Identity is null || !context.HttpContext.User.Identity.IsAuthenticated) + { + context.Result = new UnauthorizedResult(); + return; + } + + // Check if the user has the required claim + var hasClaim = context.HttpContext.User.HasClaim(CustomClaims.Permission, _permission.ToString()); + if (!hasClaim) + { + context.Result = new ForbidResult(); + return; + } + } +} diff --git a/src/Quizer.Infrastructure/Authentication/JwtTokenGenerator.cs b/src/Quizer.Infrastructure/Authentication/JwtTokenProvider.cs similarity index 88% rename from src/Quizer.Infrastructure/Authentication/JwtTokenGenerator.cs rename to src/Quizer.Infrastructure/Authentication/JwtTokenProvider.cs index 60203ab..282d368 100644 --- a/src/Quizer.Infrastructure/Authentication/JwtTokenGenerator.cs +++ b/src/Quizer.Infrastructure/Authentication/JwtTokenProvider.cs @@ -9,12 +9,12 @@ namespace Quizer.Infrastructure.Authentication; -public class JwtTokenGenerator : IJwtTokenGenerator +public class JwtTokenProvider : IJwtTokenProvider { private readonly IDateTimeProvider _dateTimeProvider; private readonly JwtSettings _jwtSettings; - public JwtTokenGenerator(IDateTimeProvider dateTimeProvider, IOptions jwtSettings) + public JwtTokenProvider(IDateTimeProvider dateTimeProvider, IOptions jwtSettings) { _dateTimeProvider = dateTimeProvider; _jwtSettings = jwtSettings.Value; @@ -93,13 +93,15 @@ public async Task ValidateRefreshToken(string refreshToken) private List CreateClaims(User user) { - return new List + var claims = new List { - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), - new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), - new Claim(JwtRegisteredClaimNames.GivenName, user.UserName!), - new Claim(JwtRegisteredClaimNames.Email, user.Email!), + new (JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new (JwtRegisteredClaimNames.Sub, user.Id.ToString()), + new (JwtRegisteredClaimNames.GivenName, user.UserName!), + new (JwtRegisteredClaimNames.Email, user.Email!), }; + + return claims; } private SigningCredentials CreateSigningCredentials() diff --git a/src/Quizer.Infrastructure/Authentication/Permission.cs b/src/Quizer.Infrastructure/Authentication/Permission.cs new file mode 100644 index 0000000..22c49ff --- /dev/null +++ b/src/Quizer.Infrastructure/Authentication/Permission.cs @@ -0,0 +1,6 @@ +namespace Quizer.Infrastructure.Authentication; + +public enum Permission +{ + CreateQuiz = 1 +} diff --git a/src/Quizer.Infrastructure/DependencyInjection.cs b/src/Quizer.Infrastructure/DependencyInjection.cs index 478ba3d..c77d8a6 100644 --- a/src/Quizer.Infrastructure/DependencyInjection.cs +++ b/src/Quizer.Infrastructure/DependencyInjection.cs @@ -82,7 +82,7 @@ public static IServiceCollection AddAuth( configuration.Bind(JwtSettings.SectionName, jwtSettings); services.AddSingleton(Options.Create(jwtSettings)); - services.AddSingleton(); + services.AddSingleton(); services .AddAuthentication(options => From 3c12f20cf229c100137b6be3f42f2dee43fbffc7 Mon Sep 17 00:00:00 2001 From: EloToJa Date: Tue, 30 Jan 2024 03:01:33 +0100 Subject: [PATCH 2/8] Add Migration --- src/Quizer.Api/ApplicationConfiguration.cs | 6 --- src/Quizer.Api/Program.cs | 1 + .../Authentication/Permission.cs | 3 +- .../MigrationExtensions.cs | 41 ++++++++++++++++++- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/Quizer.Api/ApplicationConfiguration.cs b/src/Quizer.Api/ApplicationConfiguration.cs index 5c659e2..072d061 100644 --- a/src/Quizer.Api/ApplicationConfiguration.cs +++ b/src/Quizer.Api/ApplicationConfiguration.cs @@ -1,5 +1,4 @@ using Quizer.Api.Common.Settings; -using Quizer.Infrastructure; using Serilog; namespace Quizer.Api; @@ -8,11 +7,6 @@ public static class ApplicationConfiguration { public static WebApplication UsePresentation(this WebApplication app, ConfigurationManager configuration) { - if(app.Environment.IsDevelopment()) - { - app.UseMigration(); - } - app.UseVersionedSwagger(configuration); app.UseSerilogRequestLogging(); diff --git a/src/Quizer.Api/Program.cs b/src/Quizer.Api/Program.cs index 105e169..19a33c0 100644 --- a/src/Quizer.Api/Program.cs +++ b/src/Quizer.Api/Program.cs @@ -20,6 +20,7 @@ var app = builder.Build(); { + await app.MigrateDatabase(); app.UsePresentation(builder.Configuration); } diff --git a/src/Quizer.Infrastructure/Authentication/Permission.cs b/src/Quizer.Infrastructure/Authentication/Permission.cs index 22c49ff..dd7165e 100644 --- a/src/Quizer.Infrastructure/Authentication/Permission.cs +++ b/src/Quizer.Infrastructure/Authentication/Permission.cs @@ -2,5 +2,6 @@ public enum Permission { - CreateQuiz = 1 + Admin = 0, + CreateQuiz } diff --git a/src/Quizer.Infrastructure/MigrationExtensions.cs b/src/Quizer.Infrastructure/MigrationExtensions.cs index f644171..9aa3b35 100644 --- a/src/Quizer.Infrastructure/MigrationExtensions.cs +++ b/src/Quizer.Infrastructure/MigrationExtensions.cs @@ -1,18 +1,55 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Quizer.Domain.UserAggregate; +using Quizer.Infrastructure.Authentication; using Quizer.Infrastructure.Persistance; +using System.Security.Claims; namespace Quizer.Infrastructure; public static class MigrationExtensions { - public static IApplicationBuilder UseMigration(this IApplicationBuilder app) + public static async Task MigrateDatabase(this IApplicationBuilder app) { using var scope = app.ApplicationServices.CreateScope(); using var context = scope.ServiceProvider.GetRequiredService(); context.Database.Migrate(); - return app; + await app.SeedUsers(); + } + + public static async Task SeedUsers(this IApplicationBuilder app) + { + using var scope = app.ApplicationServices.CreateScope(); + + var userManager = scope.ServiceProvider.GetRequiredService>(); + var roleManager = scope.ServiceProvider.GetRequiredService>(); + + var adminUser = await userManager.FindByNameAsync("admin"); + if(adminUser is null) + { + adminUser = new User + { + UserName = "admin", + Email = "admin@quizer.com", + }; + var result = await userManager.CreateAsync(adminUser, "Test123#"); + } + + var adminRole = await roleManager.FindByNameAsync("Administrator"); + if (adminRole is null) + { + adminRole = new IdentityRole("Administrator"); + var result = await roleManager.CreateAsync(adminRole); + + await roleManager.AddClaimAsync(adminRole, new Claim(CustomClaims.Permission, Permission.Admin.ToString())); + } + + if (adminRole.Name is not null && !await userManager.IsInRoleAsync(adminUser, adminRole.Name)) + { + var result = await userManager.AddToRoleAsync(adminUser, adminRole.Name); + } } } From ef1a01355a0df5d4152a320a196b45a9883ce73c Mon Sep 17 00:00:00 2001 From: EloToJa Date: Tue, 30 Jan 2024 03:05:11 +0100 Subject: [PATCH 3/8] Add Role enum --- src/Quizer.Infrastructure/Authentication/Role.cs | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/Quizer.Infrastructure/Authentication/Role.cs diff --git a/src/Quizer.Infrastructure/Authentication/Role.cs b/src/Quizer.Infrastructure/Authentication/Role.cs new file mode 100644 index 0000000..71afd8f --- /dev/null +++ b/src/Quizer.Infrastructure/Authentication/Role.cs @@ -0,0 +1,7 @@ +namespace Quizer.Infrastructure.Authentication; + +public enum Role +{ + Administrator = 0, + User +} From b9a3f0ff8a0464bb048ab52fac83456bc977d5b0 Mon Sep 17 00:00:00 2001 From: EloToJa Date: Tue, 30 Jan 2024 03:28:00 +0100 Subject: [PATCH 4/8] Add userId to operation --- .../Commands/Login/LoginCommandHandler.cs | 2 +- .../CreateQuiz/CreateQuizCommandHandler.cs | 2 +- .../UpdateQuiz/UpdateQuizCommandHandler.cs | 4 ++-- .../Common/Models/AggregateRoot.cs | 22 ++++++++++++++++--- src/Quizer.Domain/Common/Models/Entity.cs | 10 +-------- .../QuestionAggregate/Entities/Answer.cs | 1 - .../QuestionAggregate/Question.cs | 8 +++++-- src/Quizer.Domain/QuizAggregate/Quiz.cs | 21 +++++++++--------- .../RefreshTokenAggregate/RefreshToken.cs | 8 +++++-- src/Quizer.Domain/UserAggregate/User.cs | 2 +- 10 files changed, 47 insertions(+), 33 deletions(-) diff --git a/src/Quizer.Application/Authentication/Commands/Login/LoginCommandHandler.cs b/src/Quizer.Application/Authentication/Commands/Login/LoginCommandHandler.cs index 494ae89..cb2120a 100644 --- a/src/Quizer.Application/Authentication/Commands/Login/LoginCommandHandler.cs +++ b/src/Quizer.Application/Authentication/Commands/Login/LoginCommandHandler.cs @@ -37,7 +37,7 @@ public async Task> Handle(LoginCommand query, CancellationT string accessToken = _jwtTokenGenerator.GenerateAccessToken(user); string refreshToken = _jwtTokenGenerator.GenerateRefreshToken(user); - var token = Domain.RefreshTokenAggregate.RefreshToken.Create(refreshToken); + var token = Domain.RefreshTokenAggregate.RefreshToken.Create(user.Id, refreshToken); await _refreshTokenRepository.Add(token); return new LoginResult( diff --git a/src/Quizer.Application/Quizes/Commands/CreateQuiz/CreateQuizCommandHandler.cs b/src/Quizer.Application/Quizes/Commands/CreateQuiz/CreateQuizCommandHandler.cs index c915ed8..eca7086 100644 --- a/src/Quizer.Application/Quizes/Commands/CreateQuiz/CreateQuizCommandHandler.cs +++ b/src/Quizer.Application/Quizes/Commands/CreateQuiz/CreateQuizCommandHandler.cs @@ -33,10 +33,10 @@ public async Task> Handle(CreateQuizCommand request, Cancellatio string slug = _slugifyService.GenerateSlug(request.Name); var result = Quiz.Create( + request.UserId, request.Name, slug, request.Description, - request.UserId, userName ); diff --git a/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommandHandler.cs b/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommandHandler.cs index 6f1e444..8440015 100644 --- a/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommandHandler.cs +++ b/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommandHandler.cs @@ -7,12 +7,12 @@ namespace Quizer.Application.Quizes.Commands.UpdateQuiz; -public class UpdateQuizImageCommandHandler : IRequestHandler> +public class UpdateQuizCommandHandler : IRequestHandler> { private readonly IQuizRepository _quizRepository; private readonly ISlugifyService _slugifyService; - public UpdateQuizImageCommandHandler(IQuizRepository quizRepository, ISlugifyService slugifyService) + public UpdateQuizCommandHandler(IQuizRepository quizRepository, ISlugifyService slugifyService) { _quizRepository = quizRepository; _slugifyService = slugifyService; diff --git a/src/Quizer.Domain/Common/Models/AggregateRoot.cs b/src/Quizer.Domain/Common/Models/AggregateRoot.cs index 50bcf5e..bc5c9bc 100644 --- a/src/Quizer.Domain/Common/Models/AggregateRoot.cs +++ b/src/Quizer.Domain/Common/Models/AggregateRoot.cs @@ -1,15 +1,31 @@ -namespace Quizer.Domain.Common.Models; +using ErrorOr; + +namespace Quizer.Domain.Common.Models; public abstract class AggregateRoot : Entity where TId : AggregateRootId { public new AggregateRootId Id { get; protected set; } - protected AggregateRoot(TId id) + public DateTime CreatedAt { get; protected set; } + public Guid CreatedBy { get; protected set; } + + public DateTime UpdatedAt { get; protected set; } + public Guid UpdatedBy { get; protected set; } + + protected AggregateRoot(TId id, Guid createdBy) { Id = id; - UpdatedAt = DateTime.UtcNow; CreatedAt = DateTime.UtcNow; + UpdatedAt = DateTime.UtcNow; + CreatedBy = createdBy; + UpdatedBy = createdBy; + } + + protected void Update(Guid updatedBy) + { + UpdatedAt = DateTime.UtcNow; + UpdatedBy = updatedBy; } #pragma warning disable CS8618 diff --git a/src/Quizer.Domain/Common/Models/Entity.cs b/src/Quizer.Domain/Common/Models/Entity.cs index 146a000..183af5e 100644 --- a/src/Quizer.Domain/Common/Models/Entity.cs +++ b/src/Quizer.Domain/Common/Models/Entity.cs @@ -8,15 +8,12 @@ public abstract class Entity : IEquatable>, IHasDomainEvents { private readonly List _domainEvents = new(); public IReadOnlyList DomainEvents => _domainEvents.AsReadOnly(); + public TId Id { get; protected set; } - public DateTime CreatedAt { get; protected set; } - public DateTime UpdatedAt { get; protected set; } protected Entity(TId id) { Id = id; - CreatedAt = DateTime.UtcNow; - UpdatedAt = DateTime.UtcNow; } protected ErrorOr GetValidationErrors(ValidationResult validationResult) @@ -34,11 +31,6 @@ protected ErrorOr GetValidationErrors(ValidationResult validationResult) return errors; } - protected void Update() - { - UpdatedAt = DateTime.UtcNow; - } - public override bool Equals(object? obj) { if (obj is null || obj.GetType() != GetType()) diff --git a/src/Quizer.Domain/QuestionAggregate/Entities/Answer.cs b/src/Quizer.Domain/QuestionAggregate/Entities/Answer.cs index a8e8f92..c80baa0 100644 --- a/src/Quizer.Domain/QuestionAggregate/Entities/Answer.cs +++ b/src/Quizer.Domain/QuestionAggregate/Entities/Answer.cs @@ -27,7 +27,6 @@ public static ErrorOr Create(string answer, bool isCorrect = false) public ErrorOr Update(string answer, bool isCorrect) { - base.Update(); Text = answer; IsCorrect = isCorrect; diff --git a/src/Quizer.Domain/QuestionAggregate/Question.cs b/src/Quizer.Domain/QuestionAggregate/Question.cs index 6abd095..a761ff5 100644 --- a/src/Quizer.Domain/QuestionAggregate/Question.cs +++ b/src/Quizer.Domain/QuestionAggregate/Question.cs @@ -16,10 +16,11 @@ public sealed class Question : AggregateRoot private List _answers; private Question( + Guid userId, QuestionId id, QuizId quizId, string questionText, - List answers) : base(id) + List answers) : base(id, userId) { QuizId = quizId; QuestionText = questionText; @@ -34,11 +35,13 @@ private ErrorOr Validate() } public static ErrorOr Create( + Guid userId, QuizId quizId, string questionText, List answers) { var question = new Question( + userId, QuestionId.CreateUnique(), quizId, questionText, @@ -53,10 +56,11 @@ public static ErrorOr Create( } public ErrorOr Update( + Guid userId, string questionText, List answers) { - base.Update(); + base.Update(userId); QuestionText = questionText; _answers = answers; diff --git a/src/Quizer.Domain/QuizAggregate/Quiz.cs b/src/Quizer.Domain/QuizAggregate/Quiz.cs index f9c39c7..db87cae 100644 --- a/src/Quizer.Domain/QuizAggregate/Quiz.cs +++ b/src/Quizer.Domain/QuizAggregate/Quiz.cs @@ -11,7 +11,6 @@ public sealed class Quiz : AggregateRoot { private readonly List _questionIds = new(); - public Guid UserId { get; private set; } public string UserName { get; private set; } public string Name { get; private set; } @@ -23,18 +22,17 @@ public sealed class Quiz : AggregateRoot public IReadOnlyList QuestionIds => _questionIds.AsReadOnly(); private Quiz( + Guid userId, QuizId id, string name, string slug, string description, - Guid userId, string userName, - AverageRating averageRating) : base(id) + AverageRating averageRating) : base(id, userId) { Name = name; Slug = slug; Description = description; - UserId = userId; UserName = userName; AverageRating = averageRating; } @@ -47,18 +45,18 @@ private ErrorOr Validate() } public static ErrorOr Create( + Guid userId, string name, string slug, string description, - Guid userId, string userName) { var quiz = new Quiz( + userId, QuizId.CreateUnique(), name, slug, description, - userId, userName, AverageRating.CreateNew()); @@ -69,11 +67,12 @@ public static ErrorOr Create( } public ErrorOr Update( + Guid userId, string name, string slug, string description) { - base.Update(); + base.Update(userId); Name = name; Description = description; Slug = slug; @@ -89,15 +88,15 @@ public void Delete() this.AddDomainEvent(new QuizDeleted(this)); } - public void AddQuestion(Question question) + public void AddQuestion(Guid userId, Question question) { - base.Update(); + base.Update(userId); _questionIds.Add((QuestionId)question.Id); } - public void DeleteQuestion(Question question) + public void DeleteQuestion(Guid userId, Question question) { - base.Update(); + base.Update(userId); _questionIds.Remove((QuestionId)question.Id); } diff --git a/src/Quizer.Domain/RefreshTokenAggregate/RefreshToken.cs b/src/Quizer.Domain/RefreshTokenAggregate/RefreshToken.cs index 18a920d..ab1b128 100644 --- a/src/Quizer.Domain/RefreshTokenAggregate/RefreshToken.cs +++ b/src/Quizer.Domain/RefreshTokenAggregate/RefreshToken.cs @@ -7,26 +7,30 @@ public sealed class RefreshToken : AggregateRoot public bool IsValid { get; private set; } private RefreshToken( + Guid userId, TokenId id, bool isValid - ) : base(id) + ) : base(id, userId) { Id = id; IsValid = isValid; } public static RefreshToken Create( + Guid userId, string token) { var refreshToken = new RefreshToken( + userId, TokenId.Create(token), true); return refreshToken; } - public void Invalidate() + public void Invalidate(Guid userId) { + this.Update(userId); IsValid = false; } diff --git a/src/Quizer.Domain/UserAggregate/User.cs b/src/Quizer.Domain/UserAggregate/User.cs index e15eb00..46da11e 100644 --- a/src/Quizer.Domain/UserAggregate/User.cs +++ b/src/Quizer.Domain/UserAggregate/User.cs @@ -2,7 +2,7 @@ namespace Quizer.Domain.UserAggregate; -public class User : IdentityUser +public class User : IdentityUser { public User() : base() { From 2169a4aebb68dc2e46d644cfce5afc5a91f3f09c Mon Sep 17 00:00:00 2001 From: EloToJa Date: Tue, 30 Jan 2024 13:36:32 +0100 Subject: [PATCH 5/8] Add UserId --- .../Common/Mapping/QuizMappingConfig.cs | 19 ++++++++------ .../Controllers/V1/ApiController.cs | 10 ++++++++ .../Controllers/V1/QuestionController.cs | 6 ++--- .../Controllers/V1/QuizController.cs | 5 ++-- .../Commands/Login/LoginCommandHandler.cs | 6 ++--- .../Authentication/IJwtTokenProvider.cs | 4 +-- .../CreateQuestion/CreateQuestionCommand.cs | 1 + .../CreateQuestionCommandHandler.cs | 5 +++- .../DeleteQuestion/DeleteQuestionCommand.cs | 1 + .../DeleteQuestionCommandHandler.cs | 2 +- .../UpdateQuestion/UpdateQuestionCommand.cs | 1 + .../UpdateQuestionCommandHandler.cs | 1 + .../Commands/UpdateQuiz/UpdateQuizCommand.cs | 1 + .../UpdateQuiz/UpdateQuizCommandHandler.cs | 7 +++++- src/Quizer.Contracts/Quiz/QuizResponse.cs | 6 +++-- .../Quiz/ShortQuizResponse.cs | 6 +++-- src/Quizer.Domain/UserAggregate/User.cs | 2 +- .../Authentication/JwtTokenProvider.cs | 20 +++++++++------ .../DependencyInjection.cs | 2 +- ... 20240130123601_InitialSchema.Designer.cs} | 25 +++++++++++++------ ...ema.cs => 20240130123601_InitialSchema.cs} | 17 +++++++------ .../QuizerDbContextModelSnapshot.cs | 23 +++++++++++------ 22 files changed, 114 insertions(+), 56 deletions(-) rename src/Quizer.Infrastructure/Migrations/{20240127141250_InitialSchema.Designer.cs => 20240130123601_InitialSchema.Designer.cs} (96%) rename src/Quizer.Infrastructure/Migrations/{20240127141250_InitialSchema.cs => 20240130123601_InitialSchema.cs} (96%) diff --git a/src/Quizer.Api/Common/Mapping/QuizMappingConfig.cs b/src/Quizer.Api/Common/Mapping/QuizMappingConfig.cs index a000d9b..f701492 100644 --- a/src/Quizer.Api/Common/Mapping/QuizMappingConfig.cs +++ b/src/Quizer.Api/Common/Mapping/QuizMappingConfig.cs @@ -30,14 +30,16 @@ private static void RegisterQuestion(TypeAdapterConfig config) config.NewConfig() .Map(dest => dest, src => src); - config.NewConfig<(UpdateQuestionRequest Request, Guid QuestionId), UpdateQuestionCommand>() + config.NewConfig<(UpdateQuestionRequest Request, Guid? UserId, Guid QuestionId), UpdateQuestionCommand>() .Map(dest => dest.QuestionId, src => src.QuestionId) + .Map(dest => dest.UserId, src => src.UserId) .Map(dest => dest.QuestionText, src => src.Request.Question) .Map(dest => dest.Answers, src => src.Request.Answers) .Map(dest => dest, src => src.Request); - config.NewConfig<(CreateQuestionRequest Request, Guid QuizId), CreateQuestionCommand>() + config.NewConfig<(CreateQuestionRequest Request, Guid? UserId, Guid QuizId), CreateQuestionCommand>() .Map(dest => dest.QuizId, src => src.QuizId) + .Map(dest => dest.UserId, src => src.UserId) .Map(dest => dest.QuestionText, src => src.Request.Question) .Map(dest => dest.Answers, src => src.Request.Answers) .Map(dest => dest, src => src.Request); @@ -51,11 +53,12 @@ private static void RegisterQuestion(TypeAdapterConfig config) private static void RegisterQuiz(TypeAdapterConfig config) { - config.NewConfig<(CreateQuizRequest Request, string UserId), CreateQuizCommand>() - .Map(dest => dest.UserId, src => new Guid(src.UserId)) + config.NewConfig<(CreateQuizRequest Request, Guid? UserId), CreateQuizCommand>() + .Map(dest => dest.UserId, src => src.UserId) .Map(dest => dest, src => src.Request); - config.NewConfig<(UpdateQuizRequest Request, Guid QuizId), UpdateQuizCommand>() + config.NewConfig<(UpdateQuizRequest Request, Guid? UserId, Guid QuizId), UpdateQuizCommand>() + .Map(dest => dest.UserId, src => src.UserId) .Map(dest => dest.QuizId, src => src.QuizId) .Map(dest => dest, src => src.Request); @@ -64,13 +67,15 @@ private static void RegisterQuiz(TypeAdapterConfig config) config.NewConfig() .Map(dest => dest.Id, src => src.Id.Value.ToString()) - .Map(dest => dest.UserId, src => src.UserId.ToString()) + .Map(dest => dest.CreatedBy, src => src.CreatedBy.ToString()) + .Map(dest => dest.UpdatedBy, src => src.UpdatedBy.ToString()) .Map(dest => dest.AverageRating, src => src.AverageRating.Value) .Map(dest => dest.NumberOfRatings, src => src.AverageRating.NumRatings); config.NewConfig() .Map(dest => dest.Id, src => src.Quiz.Id.Value.ToString()) - .Map(dest => dest.UserId, src => src.Quiz.UserId.ToString()) + .Map(dest => dest.CreatedBy, src => src.Quiz.CreatedBy.ToString()) + .Map(dest => dest.UpdatedBy, src => src.Quiz.UpdatedBy.ToString()) .Map(dest => dest.AverageRating, src => src.Quiz.AverageRating.Value) .Map(dest => dest.NumberOfRatings, src => src.Quiz.AverageRating.NumRatings) .Map(dest => dest, src => src.Quiz) diff --git a/src/Quizer.Api/Controllers/V1/ApiController.cs b/src/Quizer.Api/Controllers/V1/ApiController.cs index 453ac8e..a519aca 100644 --- a/src/Quizer.Api/Controllers/V1/ApiController.cs +++ b/src/Quizer.Api/Controllers/V1/ApiController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Quizer.Api.Common.Http; +using System.Security.Claims; namespace Quizer.Api.Controllers.V1; @@ -57,4 +58,13 @@ private IActionResult ValidationProblem(List errors) } return ValidationProblem(modelState); } + + protected Guid? UserId { + get + { + string? userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + Guid? userIdGuid = userId is null ? null : new Guid(userId); + return userIdGuid; + } + } } diff --git a/src/Quizer.Api/Controllers/V1/QuestionController.cs b/src/Quizer.Api/Controllers/V1/QuestionController.cs index 92f2a54..64d89b2 100644 --- a/src/Quizer.Api/Controllers/V1/QuestionController.cs +++ b/src/Quizer.Api/Controllers/V1/QuestionController.cs @@ -40,7 +40,7 @@ public async Task CreateQuestion( Guid quizId, [FromBody] CreateQuestionRequest request) { - var command = _mapper.Map((request, quizId)); + var command = _mapper.Map((request, UserId, quizId)); var result = await _mediator.Send(command); return result.Match( @@ -53,7 +53,7 @@ public async Task UpdateQuestion( Guid questionId, [FromBody] UpdateQuestionRequest request) { - var command = _mapper.Map((request, questionId)); + var command = _mapper.Map((request, UserId, questionId)); var result = await _mediator.Send(command); return result.Match( @@ -64,7 +64,7 @@ public async Task UpdateQuestion( [HttpDelete("{questionId:guid}")] public async Task DeleteQuestion(Guid questionId) { - var command = new DeleteQuestionCommand(questionId); + var command = new DeleteQuestionCommand((Guid)UserId!, questionId); var result = await _mediator.Send(command); return result.Match( diff --git a/src/Quizer.Api/Controllers/V1/QuizController.cs b/src/Quizer.Api/Controllers/V1/QuizController.cs index 0413b98..5587b57 100644 --- a/src/Quizer.Api/Controllers/V1/QuizController.cs +++ b/src/Quizer.Api/Controllers/V1/QuizController.cs @@ -84,8 +84,7 @@ public async Task GetQuizByName(string userName, string quizSlug) public async Task CreateQuiz( CreateQuizRequest request) { - string userId = User.FindFirstValue(ClaimTypes.NameIdentifier)!; - var command = _mapper.Map((request, userId)); + var command = _mapper.Map((request, UserId)); var result = await _mediator.Send(command); return result.Match( @@ -98,7 +97,7 @@ public async Task UpdateQuiz( Guid quizId, [FromBody] UpdateQuizRequest request) { - var command = _mapper.Map((request, quizId)); + var command = _mapper.Map((request, UserId, quizId)); var result = await _mediator.Send(command); return result.Match( diff --git a/src/Quizer.Application/Authentication/Commands/Login/LoginCommandHandler.cs b/src/Quizer.Application/Authentication/Commands/Login/LoginCommandHandler.cs index cb2120a..8241ab9 100644 --- a/src/Quizer.Application/Authentication/Commands/Login/LoginCommandHandler.cs +++ b/src/Quizer.Application/Authentication/Commands/Login/LoginCommandHandler.cs @@ -34,10 +34,10 @@ public async Task> Handle(LoginCommand query, CancellationT if (!result.Succeeded) return Errors.Authentication.InvalidCredentials; - string accessToken = _jwtTokenGenerator.GenerateAccessToken(user); - string refreshToken = _jwtTokenGenerator.GenerateRefreshToken(user); + string accessToken = await _jwtTokenGenerator.GenerateAccessToken(user); + string refreshToken = await _jwtTokenGenerator.GenerateRefreshToken(user); - var token = Domain.RefreshTokenAggregate.RefreshToken.Create(user.Id, refreshToken); + var token = Domain.RefreshTokenAggregate.RefreshToken.Create(new Guid(user.Id), refreshToken); await _refreshTokenRepository.Add(token); return new LoginResult( diff --git a/src/Quizer.Application/Common/Interfaces/Authentication/IJwtTokenProvider.cs b/src/Quizer.Application/Common/Interfaces/Authentication/IJwtTokenProvider.cs index d238a6f..3436f6c 100644 --- a/src/Quizer.Application/Common/Interfaces/Authentication/IJwtTokenProvider.cs +++ b/src/Quizer.Application/Common/Interfaces/Authentication/IJwtTokenProvider.cs @@ -4,8 +4,8 @@ namespace Quizer.Application.Common.Interfaces.Authentication; public interface IJwtTokenProvider { - string GenerateAccessToken(User user); - string GenerateRefreshToken(User user); + Task GenerateAccessToken(User user); + Task GenerateRefreshToken(User user); string GenerateAccessToken(string refreshToken); Task ValidateRefreshToken(string refreshToken); } diff --git a/src/Quizer.Application/Quizes/Commands/CreateQuestion/CreateQuestionCommand.cs b/src/Quizer.Application/Quizes/Commands/CreateQuestion/CreateQuestionCommand.cs index a40b5e6..cd72be4 100644 --- a/src/Quizer.Application/Quizes/Commands/CreateQuestion/CreateQuestionCommand.cs +++ b/src/Quizer.Application/Quizes/Commands/CreateQuestion/CreateQuestionCommand.cs @@ -6,6 +6,7 @@ namespace Quizer.Application.Quizes.Commands.CreateQuestion; public record CreateQuestionCommand( + Guid UserId, Guid QuizId, string QuestionText, List Answers diff --git a/src/Quizer.Application/Quizes/Commands/CreateQuestion/CreateQuestionCommandHandler.cs b/src/Quizer.Application/Quizes/Commands/CreateQuestion/CreateQuestionCommandHandler.cs index 77fdac6..be3533a 100644 --- a/src/Quizer.Application/Quizes/Commands/CreateQuestion/CreateQuestionCommandHandler.cs +++ b/src/Quizer.Application/Quizes/Commands/CreateQuestion/CreateQuestionCommandHandler.cs @@ -27,6 +27,7 @@ public async Task> Handle(CreateQuestionCommand request, Can return Errors.Quiz.NotFound; var result = Question.Create( + request.UserId, (QuizId)quiz.Id, request.QuestionText, request.Answers @@ -37,7 +38,9 @@ public async Task> Handle(CreateQuestionCommand request, Can await _questionRepository.Add(question); - quiz.AddQuestion(question); + quiz.AddQuestion( + request.UserId, + question); return (QuestionId)question.Id; } diff --git a/src/Quizer.Application/Quizes/Commands/DeleteQuestion/DeleteQuestionCommand.cs b/src/Quizer.Application/Quizes/Commands/DeleteQuestion/DeleteQuestionCommand.cs index 69a7162..6e55cc5 100644 --- a/src/Quizer.Application/Quizes/Commands/DeleteQuestion/DeleteQuestionCommand.cs +++ b/src/Quizer.Application/Quizes/Commands/DeleteQuestion/DeleteQuestionCommand.cs @@ -5,5 +5,6 @@ namespace Quizer.Application.Quizes.Commands.DeleteQuestion; public record DeleteQuestionCommand( + Guid UserId, Guid QuestionId ) : IRequest>; diff --git a/src/Quizer.Application/Quizes/Commands/DeleteQuestion/DeleteQuestionCommandHandler.cs b/src/Quizer.Application/Quizes/Commands/DeleteQuestion/DeleteQuestionCommandHandler.cs index aadd084..dcb3683 100644 --- a/src/Quizer.Application/Quizes/Commands/DeleteQuestion/DeleteQuestionCommandHandler.cs +++ b/src/Quizer.Application/Quizes/Commands/DeleteQuestion/DeleteQuestionCommandHandler.cs @@ -25,7 +25,7 @@ public async Task> Handle(DeleteQuestionCommand request, Can var quiz = await _quizRepository.Get(question.QuizId); if (quiz is null) return Errors.Quiz.NotFound; - quiz.DeleteQuestion(question); + quiz.DeleteQuestion(request.UserId, question); _questionRepository.Delete(question); question.Delete(); diff --git a/src/Quizer.Application/Quizes/Commands/UpdateQuestion/UpdateQuestionCommand.cs b/src/Quizer.Application/Quizes/Commands/UpdateQuestion/UpdateQuestionCommand.cs index 0f32450..3b8221d 100644 --- a/src/Quizer.Application/Quizes/Commands/UpdateQuestion/UpdateQuestionCommand.cs +++ b/src/Quizer.Application/Quizes/Commands/UpdateQuestion/UpdateQuestionCommand.cs @@ -6,6 +6,7 @@ namespace Quizer.Application.Quizes.Commands.UpdateQuestion; public record UpdateQuestionCommand( + Guid UserId, Guid QuestionId, string QuestionText, List Answers diff --git a/src/Quizer.Application/Quizes/Commands/UpdateQuestion/UpdateQuestionCommandHandler.cs b/src/Quizer.Application/Quizes/Commands/UpdateQuestion/UpdateQuestionCommandHandler.cs index 6526dfd..4b61d4f 100644 --- a/src/Quizer.Application/Quizes/Commands/UpdateQuestion/UpdateQuestionCommandHandler.cs +++ b/src/Quizer.Application/Quizes/Commands/UpdateQuestion/UpdateQuestionCommandHandler.cs @@ -22,6 +22,7 @@ public async Task> Handle(UpdateQuestionCommand request, Can if (question is null) return Errors.Question.NotFound; var result = question.Update( + request.UserId, request.QuestionText, request.Answers .ConvertAll(a => Answer.Create(a.Text, a.IsCorrect).Value)); diff --git a/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommand.cs b/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommand.cs index 4adb380..4c3bba7 100644 --- a/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommand.cs +++ b/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommand.cs @@ -5,6 +5,7 @@ namespace Quizer.Application.Quizes.Commands.UpdateQuiz; public record UpdateQuizCommand( + Guid UserId, Guid QuizId, string Name, string Description) : IRequest>; diff --git a/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommandHandler.cs b/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommandHandler.cs index 8440015..fda7278 100644 --- a/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommandHandler.cs +++ b/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommandHandler.cs @@ -29,7 +29,12 @@ public async Task> Handle(UpdateQuizCommand request, Cancellatio string slug = _slugifyService.GenerateSlug(request.Name); - var result = quiz.Update(request.Name, slug, request.Description); + var result = quiz.Update( + request.UserId, + request.Name, + slug, + request.Description); + if (result.IsError) return result.Errors; return id; diff --git a/src/Quizer.Contracts/Quiz/QuizResponse.cs b/src/Quizer.Contracts/Quiz/QuizResponse.cs index f6326cf..7531361 100644 --- a/src/Quizer.Contracts/Quiz/QuizResponse.cs +++ b/src/Quizer.Contracts/Quiz/QuizResponse.cs @@ -4,7 +4,6 @@ namespace Quizer.Contracts.Quiz; public record QuizResponse( string Id, - string UserId, string UserName, string Name, string Slug, @@ -13,5 +12,8 @@ public record QuizResponse( double AverageRating, int NumberOfRatings, IReadOnlyList Questions, - DateTime CreatedAt + string CreatedBy, + string UpdatedBy, + DateTime CreatedAt, + DateTime UpdatedAt ); diff --git a/src/Quizer.Contracts/Quiz/ShortQuizResponse.cs b/src/Quizer.Contracts/Quiz/ShortQuizResponse.cs index bacaea7..40713ff 100644 --- a/src/Quizer.Contracts/Quiz/ShortQuizResponse.cs +++ b/src/Quizer.Contracts/Quiz/ShortQuizResponse.cs @@ -2,7 +2,6 @@ public record ShortQuizResponse( string Id, - string UserId, string UserName, string Name, string Slug, @@ -10,5 +9,8 @@ public record ShortQuizResponse( string Description, double AverageRating, int NumberOfRatings, - DateTime CreatedAt + string CreatedBy, + string UpdatedBy, + DateTime CreatedAt, + DateTime UpdatedAt ); diff --git a/src/Quizer.Domain/UserAggregate/User.cs b/src/Quizer.Domain/UserAggregate/User.cs index 46da11e..e15eb00 100644 --- a/src/Quizer.Domain/UserAggregate/User.cs +++ b/src/Quizer.Domain/UserAggregate/User.cs @@ -2,7 +2,7 @@ namespace Quizer.Domain.UserAggregate; -public class User : IdentityUser +public class User : IdentityUser { public User() : base() { diff --git a/src/Quizer.Infrastructure/Authentication/JwtTokenProvider.cs b/src/Quizer.Infrastructure/Authentication/JwtTokenProvider.cs index 282d368..67d1598 100644 --- a/src/Quizer.Infrastructure/Authentication/JwtTokenProvider.cs +++ b/src/Quizer.Infrastructure/Authentication/JwtTokenProvider.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Quizer.Application.Common.Interfaces.Authentication; using Quizer.Application.Common.Interfaces.Services; @@ -13,17 +14,19 @@ public class JwtTokenProvider : IJwtTokenProvider { private readonly IDateTimeProvider _dateTimeProvider; private readonly JwtSettings _jwtSettings; + private readonly UserManager _userManager; - public JwtTokenProvider(IDateTimeProvider dateTimeProvider, IOptions jwtSettings) + public JwtTokenProvider(IDateTimeProvider dateTimeProvider, IOptions jwtSettings, UserManager userManager) { _dateTimeProvider = dateTimeProvider; _jwtSettings = jwtSettings.Value; + _userManager = userManager; } - public string GenerateAccessToken(User user) + public async Task GenerateAccessToken(User user) { SigningCredentials signingCredentials = CreateSigningCredentials(); - List claims = CreateClaims(user); + List claims = await CreateClaims(user); var securityToken = new JwtSecurityToken( claims: claims, @@ -55,10 +58,10 @@ public string GenerateAccessToken(string refreshToken) return new JwtSecurityTokenHandler().WriteToken(securityToken); } - public string GenerateRefreshToken(User user) + public async Task GenerateRefreshToken(User user) { SigningCredentials signingCredentials = CreateSigningCredentials(); - List claims = CreateClaims(user); + List claims = await CreateClaims(user); var securityToken = new JwtSecurityToken( claims: claims, @@ -91,7 +94,7 @@ public async Task ValidateRefreshToken(string refreshToken) return result.IsValid; } - private List CreateClaims(User user) + private async Task> CreateClaims(User user) { var claims = new List { @@ -101,6 +104,9 @@ private List CreateClaims(User user) new (JwtRegisteredClaimNames.Email, user.Email!), }; + var userClaims = await _userManager.GetClaimsAsync(user); + claims.AddRange(userClaims); + return claims; } diff --git a/src/Quizer.Infrastructure/DependencyInjection.cs b/src/Quizer.Infrastructure/DependencyInjection.cs index c77d8a6..7b0c34d 100644 --- a/src/Quizer.Infrastructure/DependencyInjection.cs +++ b/src/Quizer.Infrastructure/DependencyInjection.cs @@ -82,7 +82,7 @@ public static IServiceCollection AddAuth( configuration.Bind(JwtSettings.SectionName, jwtSettings); services.AddSingleton(Options.Create(jwtSettings)); - services.AddSingleton(); + services.AddScoped(); services .AddAuthentication(options => diff --git a/src/Quizer.Infrastructure/Migrations/20240127141250_InitialSchema.Designer.cs b/src/Quizer.Infrastructure/Migrations/20240130123601_InitialSchema.Designer.cs similarity index 96% rename from src/Quizer.Infrastructure/Migrations/20240127141250_InitialSchema.Designer.cs rename to src/Quizer.Infrastructure/Migrations/20240130123601_InitialSchema.Designer.cs index eff6106..2fcb3dd 100644 --- a/src/Quizer.Infrastructure/Migrations/20240127141250_InitialSchema.Designer.cs +++ b/src/Quizer.Infrastructure/Migrations/20240130123601_InitialSchema.Designer.cs @@ -12,7 +12,7 @@ namespace Quizer.Infrastructure.Migrations { [DbContext(typeof(QuizerDbContext))] - [Migration("20240127141250_InitialSchema")] + [Migration("20240130123601_InitialSchema")] partial class InitialSchema { /// @@ -165,6 +165,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("QuestionText") .IsRequired() .HasMaxLength(200) @@ -176,6 +179,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); + b.Property("UpdatedBy") + .HasColumnType("uuid"); + b.HasKey("Id"); b.ToTable("Questions", (string)null); @@ -189,6 +195,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("Description") .IsRequired() .HasMaxLength(1000) @@ -206,7 +215,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); - b.Property("UserId") + b.Property("UpdatedBy") .HasColumnType("uuid"); b.Property("UserName") @@ -229,12 +238,18 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("IsValid") .HasColumnType("boolean"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); + b.Property("UpdatedBy") + .HasColumnType("uuid"); + b.HasKey("Id"); b.ToTable("RefreshTokens", (string)null); @@ -366,9 +381,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b1.Property("QuestionId") .HasColumnType("uuid"); - b1.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - b1.Property("IsCorrect") .HasColumnType("boolean"); @@ -377,9 +389,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(200) .HasColumnType("character varying(200)"); - b1.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - b1.HasKey("Id", "QuestionId"); b1.HasIndex("QuestionId"); diff --git a/src/Quizer.Infrastructure/Migrations/20240127141250_InitialSchema.cs b/src/Quizer.Infrastructure/Migrations/20240130123601_InitialSchema.cs similarity index 96% rename from src/Quizer.Infrastructure/Migrations/20240127141250_InitialSchema.cs rename to src/Quizer.Infrastructure/Migrations/20240130123601_InitialSchema.cs index 2a499db..aaa026f 100644 --- a/src/Quizer.Infrastructure/Migrations/20240127141250_InitialSchema.cs +++ b/src/Quizer.Infrastructure/Migrations/20240130123601_InitialSchema.cs @@ -59,7 +59,9 @@ protected override void Up(MigrationBuilder migrationBuilder) QuizId = table.Column(type: "uuid", nullable: false), QuestionText = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: false) }, constraints: table => { @@ -71,7 +73,6 @@ protected override void Up(MigrationBuilder migrationBuilder) columns: table => new { Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), UserName = table.Column(type: "text", nullable: false), Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), Slug = table.Column(type: "text", nullable: false), @@ -79,7 +80,9 @@ protected override void Up(MigrationBuilder migrationBuilder) AverageRating_Value = table.Column(type: "double precision", nullable: false), AverageRating_NumRatings = table.Column(type: "integer", nullable: false), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: false) }, constraints: table => { @@ -93,7 +96,9 @@ protected override void Up(MigrationBuilder migrationBuilder) Id = table.Column(type: "text", nullable: false), IsValid = table.Column(type: "boolean", nullable: false), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: false) }, constraints: table => { @@ -213,9 +218,7 @@ protected override void Up(MigrationBuilder migrationBuilder) AnswerId = table.Column(type: "uuid", nullable: false), QuestionId = table.Column(type: "uuid", nullable: false), Text = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - IsCorrect = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + IsCorrect = table.Column(type: "boolean", nullable: false) }, constraints: table => { diff --git a/src/Quizer.Infrastructure/Migrations/QuizerDbContextModelSnapshot.cs b/src/Quizer.Infrastructure/Migrations/QuizerDbContextModelSnapshot.cs index e76fcd2..f99a756 100644 --- a/src/Quizer.Infrastructure/Migrations/QuizerDbContextModelSnapshot.cs +++ b/src/Quizer.Infrastructure/Migrations/QuizerDbContextModelSnapshot.cs @@ -162,6 +162,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("QuestionText") .IsRequired() .HasMaxLength(200) @@ -173,6 +176,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); + b.Property("UpdatedBy") + .HasColumnType("uuid"); + b.HasKey("Id"); b.ToTable("Questions", (string)null); @@ -186,6 +192,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("Description") .IsRequired() .HasMaxLength(1000) @@ -203,7 +212,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); - b.Property("UserId") + b.Property("UpdatedBy") .HasColumnType("uuid"); b.Property("UserName") @@ -226,12 +235,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("IsValid") .HasColumnType("boolean"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); + b.Property("UpdatedBy") + .HasColumnType("uuid"); + b.HasKey("Id"); b.ToTable("RefreshTokens", (string)null); @@ -363,9 +378,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.Property("QuestionId") .HasColumnType("uuid"); - b1.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - b1.Property("IsCorrect") .HasColumnType("boolean"); @@ -374,9 +386,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(200) .HasColumnType("character varying(200)"); - b1.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - b1.HasKey("Id", "QuestionId"); b1.HasIndex("QuestionId"); From 5c90dd7c31ee1108ee61618ede68789972ba5465 Mon Sep 17 00:00:00 2001 From: EloToJa Date: Tue, 30 Jan 2024 14:22:22 +0100 Subject: [PATCH 6/8] Update jwt settings --- src/Quizer.Api/appsettings.Development.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Quizer.Api/appsettings.Development.json b/src/Quizer.Api/appsettings.Development.json index 289b28d..35af90a 100644 --- a/src/Quizer.Api/appsettings.Development.json +++ b/src/Quizer.Api/appsettings.Development.json @@ -27,8 +27,8 @@ }, "JwtSettings": { "Secret": "8@aeQ&V!9b^j*&kJKwebGrfQ3Tx!355q8@aeQ&V!9b^j*&kJKwebGrfQ3Tx!355q", - "Issuer": "https://quizer.com", - "Audience": "https://quizer.com" + "Issuer": "Quizer", + "Audience": "Quizer" }, "ConnectionStrings": { "DefaultConnection": "Host=localhost;Port=5432;Database=quizer;User Id=postgres;Password=postgres;" From 008540dbc4df99c2d4f256478e91940dc65cf3a5 Mon Sep 17 00:00:00 2001 From: EloToJa Date: Tue, 30 Jan 2024 14:22:36 +0100 Subject: [PATCH 7/8] Update namespaces --- src/Quizer.Api/Controllers/V1/QuizController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Quizer.Api/Controllers/V1/QuizController.cs b/src/Quizer.Api/Controllers/V1/QuizController.cs index 5587b57..65bbc99 100644 --- a/src/Quizer.Api/Controllers/V1/QuizController.cs +++ b/src/Quizer.Api/Controllers/V1/QuizController.cs @@ -13,7 +13,6 @@ using Quizer.Contracts.Common; using Quizer.Contracts.Quiz; using Quizer.Infrastructure.Authentication; -using System.Security.Claims; namespace Quizer.Api.Controllers.V1; From cb27bb93f2f436e75054a24c9d7b6ce396d31695 Mon Sep 17 00:00:00 2001 From: EloToJa Date: Tue, 30 Jan 2024 15:04:57 +0100 Subject: [PATCH 8/8] Update token provide & Add seed program --- Quizer.sln | 7 +++++ .../Authentication/JwtTokenProvider.cs | 14 ++++++++- tests/Quizer.Seed/Program.cs | 31 +++++++++++++++++++ tests/Quizer.Seed/Quizer.Seed.csproj | 14 +++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 tests/Quizer.Seed/Program.cs create mode 100644 tests/Quizer.Seed/Quizer.Seed.csproj diff --git a/Quizer.sln b/Quizer.sln index 17b1a5c..5230f61 100644 --- a/Quizer.sln +++ b/Quizer.sln @@ -25,6 +25,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{6EB218AE-AB5B-4601-A909-DDB63BB384CC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quizer.Seed", "tests\Quizer.Seed\Quizer.Seed.csproj", "{3B99FAB7-0D62-47FE-AE6E-9494AADD5CBF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -55,6 +57,10 @@ Global {6EB218AE-AB5B-4601-A909-DDB63BB384CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {6EB218AE-AB5B-4601-A909-DDB63BB384CC}.Release|Any CPU.ActiveCfg = Release|Any CPU {6EB218AE-AB5B-4601-A909-DDB63BB384CC}.Release|Any CPU.Build.0 = Release|Any CPU + {3B99FAB7-0D62-47FE-AE6E-9494AADD5CBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B99FAB7-0D62-47FE-AE6E-9494AADD5CBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B99FAB7-0D62-47FE-AE6E-9494AADD5CBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B99FAB7-0D62-47FE-AE6E-9494AADD5CBF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -65,6 +71,7 @@ Global {38AC9614-809E-419D-A209-023F9C2AFE75} = {494A7292-4421-4AEF-AB70-D0D62DC59055} {644F4CCF-BCCE-4AB5-AD20-D1D8E9174A84} = {494A7292-4421-4AEF-AB70-D0D62DC59055} {22E70F95-D56C-4122-8317-F28B3A9F9620} = {494A7292-4421-4AEF-AB70-D0D62DC59055} + {3B99FAB7-0D62-47FE-AE6E-9494AADD5CBF} = {1230AD7A-D6D4-4C7F-8817-65DDACA2C059} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F9941C6F-58D7-4463-9EA6-D40F74E8CC90} diff --git a/src/Quizer.Infrastructure/Authentication/JwtTokenProvider.cs b/src/Quizer.Infrastructure/Authentication/JwtTokenProvider.cs index 67d1598..460c8b0 100644 --- a/src/Quizer.Infrastructure/Authentication/JwtTokenProvider.cs +++ b/src/Quizer.Infrastructure/Authentication/JwtTokenProvider.cs @@ -15,12 +15,14 @@ public class JwtTokenProvider : IJwtTokenProvider private readonly IDateTimeProvider _dateTimeProvider; private readonly JwtSettings _jwtSettings; private readonly UserManager _userManager; + private readonly RoleManager _roleManager; - public JwtTokenProvider(IDateTimeProvider dateTimeProvider, IOptions jwtSettings, UserManager userManager) + public JwtTokenProvider(IDateTimeProvider dateTimeProvider, IOptions jwtSettings, UserManager userManager, RoleManager roleManager) { _dateTimeProvider = dateTimeProvider; _jwtSettings = jwtSettings.Value; _userManager = userManager; + _roleManager = roleManager; } public async Task GenerateAccessToken(User user) @@ -107,6 +109,16 @@ private async Task> CreateClaims(User user) var userClaims = await _userManager.GetClaimsAsync(user); claims.AddRange(userClaims); + var userRoles = await _userManager.GetRolesAsync(user); + foreach (var userRole in userRoles) + { + var role = await _roleManager.FindByNameAsync(userRole); + if(role is null) continue; + + var roleClaims = await _roleManager.GetClaimsAsync(role); + claims.AddRange(roleClaims); + } + return claims; } diff --git a/tests/Quizer.Seed/Program.cs b/tests/Quizer.Seed/Program.cs new file mode 100644 index 0000000..07c9eda --- /dev/null +++ b/tests/Quizer.Seed/Program.cs @@ -0,0 +1,31 @@ +using Quizer.Contracts.Authentication; +using System.Text; +using System.Text.Json; + +namespace Quizer.Seed; + +internal class Program +{ + private static readonly HttpClient _httpClient = new(); + + private static async Task Login(string email, string password) + { + var loginRequest = new LoginRequest(email, password, true); + + var stringContent = new StringContent(JsonSerializer.Serialize(loginRequest), Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync("/api/v1/auth/login", stringContent); + + string stringResponse = await response.Content.ReadAsStringAsync(); + var loginResponse = JsonSerializer.Deserialize(stringResponse); + + return loginResponse; + } + + private static void Main(string[] args) + { + _httpClient.BaseAddress = new Uri("https://localhost:7131"); + + var login = Login("admin@quizer.com", "Test123#"); + } +} diff --git a/tests/Quizer.Seed/Quizer.Seed.csproj b/tests/Quizer.Seed/Quizer.Seed.csproj new file mode 100644 index 0000000..51522ae --- /dev/null +++ b/tests/Quizer.Seed/Quizer.Seed.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + +