Skip to content

Commit

Permalink
Merge pull request #23 from bentran1vn/Feat/ScheduleBooking
Browse files Browse the repository at this point in the history
Adding Booking Slot From Student
  • Loading branch information
bentran1vn authored Oct 16, 2024
2 parents ed29e68 + 1caf1e1 commit a21317a
Show file tree
Hide file tree
Showing 12 changed files with 278 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<CustomJwtBearerEvents>();
}
}
2 changes: 1 addition & 1 deletion MBS-COMMAND.API/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion MBS-COMMAND.API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
10 changes: 10 additions & 0 deletions MBS_COMMAND.Application/Abstractions/IJwtTokenService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Security.Claims;

namespace MBS_COMMAND.Application.Abstractions;

public interface IJwtTokenService
{
string GenerateAccessToken(IEnumerable<Claim> claims);
string GenerateRefreshToken();
(ClaimsPrincipal, bool) GetPrincipalFromExpiredToken(string token);
}
Original file line number Diff line number Diff line change
@@ -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<Command.CreateScheduleCommand>
{
private readonly IRepositoryBase<User, Guid> _userRepository;
private readonly IRepositoryBase<Group, Guid> _groupRepository;
private readonly IRepositoryBase<Slot, Guid> _slotRepository;
private readonly IRepositoryBase<Subject, Guid> _subjectRepository;
private readonly IRepositoryBase<Schedule, Guid> _scheduleRepository;

public CreateScheduleCommandHandler(IRepositoryBase<User, Guid> userRepository, IRepositoryBase<Group, Guid> groupRepository, IRepositoryBase<Slot, Guid> slotRepository, IRepositoryBase<Subject, Guid> subjectRepository, IRepositoryBase<Schedule, Guid> scheduleRepository)
{
_userRepository = userRepository;
_groupRepository = groupRepository;
_slotRepository = slotRepository;
_subjectRepository = subjectRepository;
_scheduleRepository = scheduleRepository;
}

public async Task<Result> 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 !");
}
}
20 changes: 20 additions & 0 deletions MBS_COMMAND.Contract/Services/Schedule/Command.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using FluentValidation;

namespace MBS_COMMAND.Contract.Services.Schedule.Validators;

public class CreateScheduleValidators : AbstractValidator<Command.CreateScheduleCommand>
{
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();
}
}
1 change: 1 addition & 0 deletions MBS_COMMAND.Domain/Entities/Schedule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class Schedule : Entity<Guid>, 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; }
Expand Down
2 changes: 0 additions & 2 deletions MBS_COMMAND.Domain/Entities/Subject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ public class Subject : Entity<Guid>, 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 ; }
}
97 changes: 97 additions & 0 deletions MBS_COMMAND.Infrastucture/Authentication/JwtTokenService.cs
Original file line number Diff line number Diff line change
@@ -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<Claim> 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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,6 +22,7 @@ public static class ServiceCollectionExtensions
{
public static void AddServicesInfrastructure(this IServiceCollection services)
=> services
.AddTransient<IJwtTokenService, JwtTokenService>()
.AddTransient<ICacheService, CacheService>()
.AddSingleton<IMediaService, CloudinaryService>()
.AddSingleton<IMailService, MailService>()
Expand Down
37 changes: 37 additions & 0 deletions MBS_COMMAND.Presentation/APIs/Schedules/SchedulesApi.cs
Original file line number Diff line number Diff line change
@@ -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<IResult> 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);
}
}

0 comments on commit a21317a

Please sign in to comment.