From 1caf1e10fdecac7de9e715e1216a624e53939546 Mon Sep 17 00:00:00 2001 From: bentran1vn Date: Wed, 16 Oct 2024 22:38:15 +0700 Subject: [PATCH] Adding Booking Slot From Student --- .../Extensions/JwtExtensions.cs | 8 +- MBS-COMMAND.API/appsettings.Development.json | 2 +- MBS-COMMAND.API/appsettings.json | 2 +- .../Abstractions/IJwtTokenService.cs | 10 ++ .../Schedules/CreateScheduleCommandHandler.cs | 88 +++++++++++++++++ .../Services/Schedule/Command.cs | 20 ++++ .../Validators/CreateScheduleValidators.cs | 14 +++ MBS_COMMAND.Domain/Entities/Schedule.cs | 1 + MBS_COMMAND.Domain/Entities/Subject.cs | 2 - .../Authentication/JwtTokenService.cs | 97 +++++++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 2 + .../APIs/Schedules/SchedulesApi.cs | 37 +++++++ 12 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 MBS_COMMAND.Application/Abstractions/IJwtTokenService.cs create mode 100644 MBS_COMMAND.Application/UserCases/Commands/Schedules/CreateScheduleCommandHandler.cs create mode 100644 MBS_COMMAND.Contract/Services/Schedule/Command.cs create mode 100644 MBS_COMMAND.Contract/Services/Schedule/Validators/CreateScheduleValidators.cs create mode 100644 MBS_COMMAND.Infrastucture/Authentication/JwtTokenService.cs create mode 100644 MBS_COMMAND.Presentation/APIs/Schedules/SchedulesApi.cs diff --git a/MBS-COMMAND.API/DependencyInjection/Extensions/JwtExtensions.cs b/MBS-COMMAND.API/DependencyInjection/Extensions/JwtExtensions.cs index 2d4c11f..324eb47 100644 --- a/MBS-COMMAND.API/DependencyInjection/Extensions/JwtExtensions.cs +++ b/MBS-COMMAND.API/DependencyInjection/Extensions/JwtExtensions.cs @@ -1,5 +1,6 @@ using System.Text; using MBS_COMMAND.Infrastucture.DependencyInjection.Options; +using MBS_COMMAND.Presentation.Constrants; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; @@ -58,7 +59,12 @@ public static void AddJwtAuthenticationAPI(this IServiceCollection services, ICo //o.EventsType = typeof(CustomJwtBearerEvents); }); - services.AddAuthorization(); + services.AddAuthorization(opts => + { + opts.AddPolicy(RoleNames.Student, policy => policy.RequireRole("0")); + opts.AddPolicy(RoleNames.Mentor, policy => policy.RequireRole("1")); + opts.AddPolicy(RoleNames.Admin, policy => policy.RequireRole("2")); + }); // services.AddScoped(); } } \ No newline at end of file diff --git a/MBS-COMMAND.API/appsettings.Development.json b/MBS-COMMAND.API/appsettings.Development.json index 4f69f9b..60f238e 100644 --- a/MBS-COMMAND.API/appsettings.Development.json +++ b/MBS-COMMAND.API/appsettings.Development.json @@ -6,7 +6,7 @@ "JwtOption": { "Issuer": "http://103.162.14.116:8080", "Audience": "http://103.162.14.116:8080", - "SecretKey": "oEZqUKrrKDKP7A9OtrB4GfPGJ92vLDpKakaka", + "SecretKey": "IRanUIwukUBzSauFsZnr7AjV7XS96sun", "ExpireMin": 5 }, "MasstransitConfiguration": { diff --git a/MBS-COMMAND.API/appsettings.json b/MBS-COMMAND.API/appsettings.json index 5436af7..7bd0805 100644 --- a/MBS-COMMAND.API/appsettings.json +++ b/MBS-COMMAND.API/appsettings.json @@ -6,7 +6,7 @@ "JwtOption": { "Issuer": "http://103.162.14.116:8080", "Audience": "http://103.162.14.116:8080", - "SecretKey": "oEZqUKrrKDKP7A9OtrB4GfPGJ92vLDpKakaka", + "SecretKey": "IRanUIwukUBzSauFsZnr7AjV7XS96sun", "ExpireMin": 5 }, "MasstransitConfiguration": { diff --git a/MBS_COMMAND.Application/Abstractions/IJwtTokenService.cs b/MBS_COMMAND.Application/Abstractions/IJwtTokenService.cs new file mode 100644 index 0000000..e4ad652 --- /dev/null +++ b/MBS_COMMAND.Application/Abstractions/IJwtTokenService.cs @@ -0,0 +1,10 @@ +using System.Security.Claims; + +namespace MBS_COMMAND.Application.Abstractions; + +public interface IJwtTokenService +{ + string GenerateAccessToken(IEnumerable claims); + string GenerateRefreshToken(); + (ClaimsPrincipal, bool) GetPrincipalFromExpiredToken(string token); +} \ No newline at end of file diff --git a/MBS_COMMAND.Application/UserCases/Commands/Schedules/CreateScheduleCommandHandler.cs b/MBS_COMMAND.Application/UserCases/Commands/Schedules/CreateScheduleCommandHandler.cs new file mode 100644 index 0000000..471a016 --- /dev/null +++ b/MBS_COMMAND.Application/UserCases/Commands/Schedules/CreateScheduleCommandHandler.cs @@ -0,0 +1,88 @@ +using MBS_COMMAND.Contract.Abstractions.Messages; +using MBS_COMMAND.Contract.Abstractions.Shared; +using MBS_COMMAND.Contract.Services.Schedule; +using MBS_COMMAND.Domain.Abstractions.Repositories; +using MBS_COMMAND.Domain.Entities; + +namespace MBS_COMMAND.Application.UserCases.Commands.Schedules; + +public class CreateScheduleCommandHandler : ICommandHandler +{ + private readonly IRepositoryBase _userRepository; + private readonly IRepositoryBase _groupRepository; + private readonly IRepositoryBase _slotRepository; + private readonly IRepositoryBase _subjectRepository; + private readonly IRepositoryBase _scheduleRepository; + + public CreateScheduleCommandHandler(IRepositoryBase userRepository, IRepositoryBase groupRepository, IRepositoryBase slotRepository, IRepositoryBase subjectRepository, IRepositoryBase scheduleRepository) + { + _userRepository = userRepository; + _groupRepository = groupRepository; + _slotRepository = slotRepository; + _subjectRepository = subjectRepository; + _scheduleRepository = scheduleRepository; + } + + public async Task Handle(Command.CreateScheduleCommand request, CancellationToken cancellationToken) + { + var user = await _userRepository.FindByIdAsync(request.UserId, cancellationToken); + + if (user == null || user.IsDeleted) + { + return Result.Failure(new Error("400", "User is not exist !")); + } + + var group = await _groupRepository.FindSingleAsync(x => x.LeaderId.Equals(user.Id), cancellationToken); + + if (group == null || group.IsDeleted) + { + return Result.Failure(new Error("403", "Must own a group !")); + } + + var slot = await _slotRepository.FindByIdAsync(request.SlotId, cancellationToken); + + if (slot == null || group.IsDeleted) + { + return Result.Failure(new Error("400", "Slot is not exist !")); + } + + if (slot.IsBook) + { + return Result.Failure(new Error("403", "Slot is booked !")); + } + + var subject = await _subjectRepository.FindByIdAsync(request.SubjectId, cancellationToken); + + if (subject == null || subject.IsDeleted) + { + return Result.Failure(new Error("400", "Subject is not exist !")); + } + + var start = TimeOnly.Parse(request.StartTime); + var end = TimeOnly.Parse(request.EndTime); + + if (start.CompareTo(slot.StartTime) < 0 || + end.CompareTo(slot.EndTime) > 0) + { + return Result.Failure(new Error("500", "Invalid booking time !")); + } + + var schedule = new Schedule() + { + Id = Guid.NewGuid(), + StartTime = start, + EndTime = end, + Date = slot.Date, + MentorId = slot.MentorId ?? new Guid(), + SubjectId = request.SubjectId, + GroupId = group.Id, + IsBooked = true, + }; + + slot.IsBook = true; + + _scheduleRepository.Add(schedule); + + return Result.Success("Booking Schedule Successfully !"); + } +} \ No newline at end of file diff --git a/MBS_COMMAND.Contract/Services/Schedule/Command.cs b/MBS_COMMAND.Contract/Services/Schedule/Command.cs new file mode 100644 index 0000000..b436406 --- /dev/null +++ b/MBS_COMMAND.Contract/Services/Schedule/Command.cs @@ -0,0 +1,20 @@ +using System.ComponentModel; +using MBS_COMMAND.Contract.Abstractions.Messages; +using Swashbuckle.AspNetCore.Annotations; + +namespace MBS_COMMAND.Contract.Services.Schedule; + +public class Command +{ + public record CreateScheduleCommand : ICommand + { + [SwaggerSchema(ReadOnly = true)] + [DefaultValue("e824c924-e441-4367-a03b-8dd13223f76f")] + public Guid UserId { get; set; } + + public Guid SlotId { get; set; } + public Guid SubjectId { get; set; } + public string StartTime { get; set; } + public string EndTime { get; set; } + } +} \ No newline at end of file diff --git a/MBS_COMMAND.Contract/Services/Schedule/Validators/CreateScheduleValidators.cs b/MBS_COMMAND.Contract/Services/Schedule/Validators/CreateScheduleValidators.cs new file mode 100644 index 0000000..84e8d48 --- /dev/null +++ b/MBS_COMMAND.Contract/Services/Schedule/Validators/CreateScheduleValidators.cs @@ -0,0 +1,14 @@ +using FluentValidation; + +namespace MBS_COMMAND.Contract.Services.Schedule.Validators; + +public class CreateScheduleValidators : AbstractValidator +{ + public CreateScheduleValidators() + { + RuleFor(x => x.StartTime).NotEmpty().LessThan(x => x.EndTime); + RuleFor(x => x.EndTime).NotEmpty().GreaterThan(x => x.StartTime); + RuleFor(x => x.SlotId).NotEmpty(); + RuleFor(x => x.SubjectId).NotEmpty(); + } +} \ No newline at end of file diff --git a/MBS_COMMAND.Domain/Entities/Schedule.cs b/MBS_COMMAND.Domain/Entities/Schedule.cs index be1bd1f..b44e3d2 100644 --- a/MBS_COMMAND.Domain/Entities/Schedule.cs +++ b/MBS_COMMAND.Domain/Entities/Schedule.cs @@ -9,6 +9,7 @@ public class Schedule : Entity, IAuditableEntity public Guid GroupId { get; set; } public virtual Group? Group { get; set; } + public TimeOnly StartTime { get; set; } public TimeOnly EndTime { get; set; } public DateOnly Date { get; set; } diff --git a/MBS_COMMAND.Domain/Entities/Subject.cs b/MBS_COMMAND.Domain/Entities/Subject.cs index 0172ed3..8614cb1 100644 --- a/MBS_COMMAND.Domain/Entities/Subject.cs +++ b/MBS_COMMAND.Domain/Entities/Subject.cs @@ -13,8 +13,6 @@ public class Subject : Entity, IAuditableEntity public int Status { get ; set ; } public Guid SemesterId { get; set; } public virtual Semester? Semester { get; set; } - - public DateTimeOffset CreatedOnUtc { get ; set ; } public DateTimeOffset? ModifiedOnUtc { get ; set ; } } diff --git a/MBS_COMMAND.Infrastucture/Authentication/JwtTokenService.cs b/MBS_COMMAND.Infrastucture/Authentication/JwtTokenService.cs new file mode 100644 index 0000000..2ceb3d8 --- /dev/null +++ b/MBS_COMMAND.Infrastucture/Authentication/JwtTokenService.cs @@ -0,0 +1,97 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using MBS_COMMAND.Application.Abstractions; +using MBS_COMMAND.Infrastucture.DependencyInjection.Options; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; + +namespace MBS_COMMAND.Infrastucture.Authentication; + +public class JwtTokenService : IJwtTokenService +{ + private readonly JwtOption jwtOption = new JwtOption(); + + public JwtTokenService(IConfiguration configuration) + { + configuration.GetSection(nameof(JwtOption)).Bind(jwtOption); + } + + public string GenerateAccessToken(IEnumerable claims) + { + var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOption.SecretKey)); + var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); + + var tokeOptions = new JwtSecurityToken( + issuer: jwtOption.Issuer, + audience: jwtOption.Audience, + claims: claims, + expires: DateTime.Now.AddMinutes(jwtOption.ExpireMin), + signingCredentials: signinCredentials + ); + + var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions); + return tokenString; + } + + public string GenerateRefreshToken() + { + var randomNumber = new byte[32]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(randomNumber); + return Convert.ToBase64String(randomNumber); + } + } + + public (ClaimsPrincipal, bool) GetPrincipalFromExpiredToken(string token) + { + var Key = Encoding.UTF8.GetBytes(jwtOption.SecretKey); + + var tokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Key), + ClockSkew = TimeSpan.Zero + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + + try + { + // First, try to validate the token with lifetime validation + tokenValidationParameters.ValidateLifetime = true; + var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken securityToken); + + if (!(securityToken is JwtSecurityToken jwtSecurityToken) || + !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) + { + throw new SecurityTokenException("Invalid token"); + } + + return (principal, false); // Token is valid and not expired + } + catch (SecurityTokenExpiredException) + { + // Token is expired, validate without lifetime check + tokenValidationParameters.ValidateLifetime = false; + var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken securityToken); + + if (!(securityToken is JwtSecurityToken jwtSecurityToken) || + !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) + { + throw new SecurityTokenException("Invalid token"); + } + + return (principal, true); // Token is valid but expired + } + catch (Exception) + { + // Any other exception means the token is invalid + throw new SecurityTokenException("Invalid token"); + } + } +} \ No newline at end of file diff --git a/MBS_COMMAND.Infrastucture/DependencyInjection/Extensions/ServiceCollectionExtensions.cs b/MBS_COMMAND.Infrastucture/DependencyInjection/Extensions/ServiceCollectionExtensions.cs index 94db6d8..256e438 100644 --- a/MBS_COMMAND.Infrastucture/DependencyInjection/Extensions/ServiceCollectionExtensions.cs +++ b/MBS_COMMAND.Infrastucture/DependencyInjection/Extensions/ServiceCollectionExtensions.cs @@ -13,6 +13,7 @@ using MBS_COMMAND.Infrastucture.DependencyInjection.Options; using MBS_COMMAND.Infrastucture.PipeObservers; using MBS_COMMAND.Infrastucture.Media; +using MBS_COMMAND.Infrastucture.Authentication; using Microsoft.Extensions.Options; namespace MBS_COMMAND.Infrastucture.DependencyInjection.Extensions; @@ -21,6 +22,7 @@ public static class ServiceCollectionExtensions { public static void AddServicesInfrastructure(this IServiceCollection services) => services + .AddTransient() .AddTransient() .AddSingleton() .AddSingleton() diff --git a/MBS_COMMAND.Presentation/APIs/Schedules/SchedulesApi.cs b/MBS_COMMAND.Presentation/APIs/Schedules/SchedulesApi.cs new file mode 100644 index 0000000..229558b --- /dev/null +++ b/MBS_COMMAND.Presentation/APIs/Schedules/SchedulesApi.cs @@ -0,0 +1,37 @@ +using MBS_COMMAND.Application.Abstractions; +using MBS_COMMAND.Contract.Services.Schedule; +using MBS_COMMAND.Presentation.Abstractions; +using MBS_COMMAND.Presentation.Constrants; +using Microsoft.AspNetCore.Authentication; + +namespace MBS_COMMAND.Presentation.APIs.Schedules; + +public class SchedulesApi : ApiEndpoint, ICarterModule +{ + private const string BaseUrl = "/api/v{version:apiVersion}/schedules"; + + public void AddRoutes(IEndpointRouteBuilder app) + { + var gr1 = app.NewVersionedApi("Schedules") + .MapGroup(BaseUrl).HasApiVersion(1); + + gr1.MapPost("", CreateSchedules).RequireAuthorization(RoleNames.Student); + } + + public static async Task CreateSchedules(ISender sender, HttpContext context, IJwtTokenService jwtTokenService, + [FromBody] Command.CreateScheduleCommand command) + { + var accessToken = await context.GetTokenAsync("access_token"); + var (claimPrincipal, _) = jwtTokenService.GetPrincipalFromExpiredToken(accessToken!); + var userId = claimPrincipal.Claims.FirstOrDefault(c => c.Type == "UserId")!.Value; + + command.UserId = new Guid(userId); + + var result = await sender.Send(command); + + if (result.IsFailure) + return HandlerFailure(result); + + return Results.Ok(result); + } +} \ No newline at end of file