From 8d0f1898246943edb2017af1e6ac1860342817e7 Mon Sep 17 00:00:00 2001 From: saijaku0 Date: Wed, 18 Feb 2026 22:02:04 +0100 Subject: [PATCH 1/3] feat: add xml documentation --- .../Common/Extension/AppointmentQueryExtensions.cs | 7 +++++++ src/Booking/Booking.Domain/Entities/ApplicationUser.cs | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Booking/Booking.Application/Common/Extension/AppointmentQueryExtensions.cs b/src/Booking/Booking.Application/Common/Extension/AppointmentQueryExtensions.cs index 029b18d..f7a9a8d 100644 --- a/src/Booking/Booking.Application/Common/Extension/AppointmentQueryExtensions.cs +++ b/src/Booking/Booking.Application/Common/Extension/AppointmentQueryExtensions.cs @@ -4,6 +4,13 @@ namespace Booking.Application.Common.Extension { public static class AppointmentQueryExtensions { + /// + /// Filters appointments that overlap with the specified time interval. + /// + /// Optional doctor ID to filter by. If null, all doctors are considered. + /// Start of the interval (exclusive on the right). + /// End of the interval (exclusive on the left). + /// An IQueryable of appointments that overlap with the interval. public static IQueryable WhereOverlaps( this IQueryable query, Guid? doctorId, diff --git a/src/Booking/Booking.Domain/Entities/ApplicationUser.cs b/src/Booking/Booking.Domain/Entities/ApplicationUser.cs index 8e69ec5..b3e5eef 100644 --- a/src/Booking/Booking.Domain/Entities/ApplicationUser.cs +++ b/src/Booking/Booking.Domain/Entities/ApplicationUser.cs @@ -61,7 +61,7 @@ public static ApplicationUser CreatePatient( string lastName, string email, string phoneNumber, - string adress) + string address) { return new ApplicationUser { @@ -70,7 +70,7 @@ public static ApplicationUser CreatePatient( FirstName = firstName, LastName = lastName, PhoneNumber = phoneNumber, - Address = adress, + Address = address, EmailConfirmed = true }; } From 134daca5c5eb63c6e4a5adee9d844e0474205b9f Mon Sep 17 00:00:00 2001 From: saijaku0 Date: Wed, 18 Feb 2026 22:52:45 +0100 Subject: [PATCH 2/3] fix: update validation behevior and implement new methods --- src/Booking/Booking.API/Program.cs | 3 +- .../Common/Behaviors/AuthorizationBehavior.cs | 59 +++++++++++++++++++ .../Common/Security/AuthorizeAttribute.cs | 20 +++++++ .../CreateDoctor/CreateDoctorCommand.cs | 5 +- .../DeleteDoctor/DeleteDoctorCommand.cs | 5 +- .../UpdateDoctor/UpdateDoctorCommand.cs | 5 +- .../UpdateDoctorPhotoCommand.cs | 5 +- .../UpdateScheduleConfigCommand.cs | 5 +- .../UpdateScheduleConfigCommandHandler.cs | 23 +++++++- .../RegisterUser/RegisterUserCommand.cs | 18 +++--- .../RegisterUserCommandHandler.cs | 2 +- 11 files changed, 132 insertions(+), 18 deletions(-) create mode 100644 src/Booking/Booking.Application/Common/Behaviors/AuthorizationBehavior.cs create mode 100644 src/Booking/Booking.Application/Common/Security/AuthorizeAttribute.cs diff --git a/src/Booking/Booking.API/Program.cs b/src/Booking/Booking.API/Program.cs index ea5f6f1..79a242e 100644 --- a/src/Booking/Booking.API/Program.cs +++ b/src/Booking/Booking.API/Program.cs @@ -42,7 +42,8 @@ { cfg.RegisterServicesFromAssembly(typeof(CreateAppointmentCommand).Assembly); - cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); + cfg.AddBehavior(typeof(IPipelineBehavior<,>)); + cfg.AddOpenBehaviors([typeof(ValidationBehavior<,>), typeof(AuthorizationBehavior<,>)]); }); builder.Services.AddCors(options => diff --git a/src/Booking/Booking.Application/Common/Behaviors/AuthorizationBehavior.cs b/src/Booking/Booking.Application/Common/Behaviors/AuthorizationBehavior.cs new file mode 100644 index 0000000..e934428 --- /dev/null +++ b/src/Booking/Booking.Application/Common/Behaviors/AuthorizationBehavior.cs @@ -0,0 +1,59 @@ +using Booking.Application.Common.Interfaces; +using Booking.Application.Common.Security; +using Booking.Application.Doctors.Command.DeleteDoctor; +using MediatR; +using System.Reflection; + +namespace Booking.Application.Common.Behaviors +{ + public class AuthorizationBehavior( + ICurrentUserService userService, + IIdentityService identityService) + : IPipelineBehavior where TRequest : notnull + { + private readonly ICurrentUserService _userService = userService; + private readonly IIdentityService _identityService = identityService; + + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + var authorizeAttributes = request + .GetType().GetCustomAttributes() + .ToList(); + + if (!authorizeAttributes.Any()) + return await next(); + + var userId = _userService.UserId; + if (string.IsNullOrEmpty(userId)) + throw new UnauthorizedAccessException(); + + var attributesWithRoles = authorizeAttributes + .Where((a => a.Roles != null && a.Roles.Length > 0)) + .ToList(); + + if (!attributesWithRoles.Any()) + return await next(); + + + if (await IsUserInAnyRoleAsync(userId, attributesWithRoles)) + return await next(); + + throw new ForbiddenAccessException(); + } + + private async Task IsUserInAnyRoleAsync( + string userId, + IEnumerable attributesWithRoles) + { + foreach (var attr in attributesWithRoles) + foreach (var role in attr.Roles!) + if (await _identityService.IsInRoleAsync(userId, role.Trim())) + return true; + + return false; + } + } +} diff --git a/src/Booking/Booking.Application/Common/Security/AuthorizeAttribute.cs b/src/Booking/Booking.Application/Common/Security/AuthorizeAttribute.cs new file mode 100644 index 0000000..5772f2d --- /dev/null +++ b/src/Booking/Booking.Application/Common/Security/AuthorizeAttribute.cs @@ -0,0 +1,20 @@ +namespace Booking.Application.Common.Security +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] + public class AuthorizeAttribute : Attribute + { + private string[]? _roles; + public string[]? Roles { + get => _roles; + set + { + if (value != null && value.Any(string.IsNullOrWhiteSpace)) + throw new ArgumentException("Role names cannot be null or whitespace.", nameof(Roles)); + _roles = value; + } + } + + // Now it's useless but in future + public string Policy { get; set; } + } +} diff --git a/src/Booking/Booking.Application/Doctors/Command/CreateDoctor/CreateDoctorCommand.cs b/src/Booking/Booking.Application/Doctors/Command/CreateDoctor/CreateDoctorCommand.cs index 77a2eac..1b0e846 100644 --- a/src/Booking/Booking.Application/Doctors/Command/CreateDoctor/CreateDoctorCommand.cs +++ b/src/Booking/Booking.Application/Doctors/Command/CreateDoctor/CreateDoctorCommand.cs @@ -1,7 +1,10 @@ -using MediatR; +using Booking.Application.Common.Security; +using Booking.Domain.Constants; +using MediatR; namespace Booking.Application.Doctors.Command.CreateDoctor { + [Authorize(Roles = [Roles.Admin])] public record CreateDoctorCommand( string Email, string Password, diff --git a/src/Booking/Booking.Application/Doctors/Command/DeleteDoctor/DeleteDoctorCommand.cs b/src/Booking/Booking.Application/Doctors/Command/DeleteDoctor/DeleteDoctorCommand.cs index 593c618..45ead24 100644 --- a/src/Booking/Booking.Application/Doctors/Command/DeleteDoctor/DeleteDoctorCommand.cs +++ b/src/Booking/Booking.Application/Doctors/Command/DeleteDoctor/DeleteDoctorCommand.cs @@ -1,6 +1,9 @@ -using MediatR; +using Booking.Application.Common.Security; +using Booking.Domain.Constants; +using MediatR; namespace Booking.Application.Doctors.Command.DeleteDoctor { + [Authorize(Roles = [Roles.Admin])] public record DeleteDoctorCommand(Guid DoctorId) : IRequest; } diff --git a/src/Booking/Booking.Application/Doctors/Command/UpdateDoctor/UpdateDoctorCommand.cs b/src/Booking/Booking.Application/Doctors/Command/UpdateDoctor/UpdateDoctorCommand.cs index 0d0882d..cb4c500 100644 --- a/src/Booking/Booking.Application/Doctors/Command/UpdateDoctor/UpdateDoctorCommand.cs +++ b/src/Booking/Booking.Application/Doctors/Command/UpdateDoctor/UpdateDoctorCommand.cs @@ -1,7 +1,10 @@ -using MediatR; +using Booking.Application.Common.Security; +using Booking.Domain.Constants; +using MediatR; namespace Booking.Application.Doctors.Command.UpdateDoctor { + [Authorize(Roles = [Roles.Admin, Roles.Doctor])] public record UpdateDoctorCommand( Guid UserId, string Name, diff --git a/src/Booking/Booking.Application/Doctors/Command/UpdateProfilePhoto/UpdateDoctorPhotoCommand.cs b/src/Booking/Booking.Application/Doctors/Command/UpdateProfilePhoto/UpdateDoctorPhotoCommand.cs index e7cf6e0..5cbbe6d 100644 --- a/src/Booking/Booking.Application/Doctors/Command/UpdateProfilePhoto/UpdateDoctorPhotoCommand.cs +++ b/src/Booking/Booking.Application/Doctors/Command/UpdateProfilePhoto/UpdateDoctorPhotoCommand.cs @@ -1,7 +1,10 @@ -using MediatR; +using Booking.Application.Common.Security; +using Booking.Domain.Constants; +using MediatR; namespace Booking.Application.Doctors.Command.UpdateProfilePhoto { + [Authorize(Roles = [Roles.Admin, Roles.Doctor])] public record UpdateDoctorPhotoCommand( Stream PhotoStream, string FileName, diff --git a/src/Booking/Booking.Application/Doctors/Command/UpdateScheduleConfig/UpdateScheduleConfigCommand.cs b/src/Booking/Booking.Application/Doctors/Command/UpdateScheduleConfig/UpdateScheduleConfigCommand.cs index 4519f48..64aec9d 100644 --- a/src/Booking/Booking.Application/Doctors/Command/UpdateScheduleConfig/UpdateScheduleConfigCommand.cs +++ b/src/Booking/Booking.Application/Doctors/Command/UpdateScheduleConfig/UpdateScheduleConfigCommand.cs @@ -1,7 +1,10 @@ -using MediatR; +using Booking.Application.Common.Security; +using Booking.Domain.Constants; +using MediatR; namespace Booking.Application.Doctors.Command.UpdateScheduleConfig { + [Authorize(Roles = [Roles.Doctor])] public record UpdateScheduleConfigCommand( Guid DoctorId, TimeSpan DayStart, diff --git a/src/Booking/Booking.Application/Doctors/Command/UpdateScheduleConfig/UpdateScheduleConfigCommandHandler.cs b/src/Booking/Booking.Application/Doctors/Command/UpdateScheduleConfig/UpdateScheduleConfigCommandHandler.cs index bc52a58..d89ef39 100644 --- a/src/Booking/Booking.Application/Doctors/Command/UpdateScheduleConfig/UpdateScheduleConfigCommandHandler.cs +++ b/src/Booking/Booking.Application/Doctors/Command/UpdateScheduleConfig/UpdateScheduleConfigCommandHandler.cs @@ -1,15 +1,34 @@ using Booking.Application.Common.Interfaces; +using Booking.Application.Doctors.Command.DeleteDoctor; using Booking.Domain.Entities; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Booking.Application.Doctors.Command.UpdateScheduleConfig { public class UpdateScheduleConfigCommandHandler( - IBookingDbContext context) : IRequestHandler + IBookingDbContext context, + ICurrentUserService userService) + : IRequestHandler { private readonly IBookingDbContext _context = context; - public async Task Handle(UpdateScheduleConfigCommand request, CancellationToken cancellationToken) + private readonly ICurrentUserService _userService = userService; + public async Task Handle( + UpdateScheduleConfigCommand request, + CancellationToken cancellationToken) { + var currentUserId = _userService.UserId; + if (string.IsNullOrWhiteSpace(currentUserId)) + throw new UnauthorizedAccessException(); + + var doctorId = await _context.Doctors + .AsNoTracking() + .FirstOrDefaultAsync(d => d.ApplicationUserId == currentUserId, cancellationToken) + ?? throw new ForbiddenAccessException("Current user is not a doctor."); + + if (request.DoctorId != doctorId.Id) + throw new ForbiddenAccessException("You can only edit your own schedule."); + var config = _context.DoctorScheduleConfigs .FirstOrDefault(x => x.DoctorId == request.DoctorId); diff --git a/src/Booking/Booking.Application/Identity/Commands/RegisterUser/RegisterUserCommand.cs b/src/Booking/Booking.Application/Identity/Commands/RegisterUser/RegisterUserCommand.cs index 43f199d..916df61 100644 --- a/src/Booking/Booking.Application/Identity/Commands/RegisterUser/RegisterUserCommand.cs +++ b/src/Booking/Booking.Application/Identity/Commands/RegisterUser/RegisterUserCommand.cs @@ -5,13 +5,13 @@ namespace Booking.Application.Identity.Commands.RegisterUser { public record RegisterUserCommand( - string UserName, - string UserSurname, - string UserEmail, - string UserPassword, - DateOnly DateOfBirth, - Gender Gender, - string? PhoneNumber, - string? Address -) : IRequest; + string UserName, + string UserSurname, + string UserEmail, + string UserPassword, + DateOnly DateOfBirth, + Gender Gender, + string? PhoneNumber, + string? Address + ) : IRequest; } diff --git a/src/Booking/Booking.Application/Identity/Commands/RegisterUser/RegisterUserCommandHandler.cs b/src/Booking/Booking.Application/Identity/Commands/RegisterUser/RegisterUserCommandHandler.cs index b49ac4a..72b2c72 100644 --- a/src/Booking/Booking.Application/Identity/Commands/RegisterUser/RegisterUserCommandHandler.cs +++ b/src/Booking/Booking.Application/Identity/Commands/RegisterUser/RegisterUserCommandHandler.cs @@ -26,7 +26,7 @@ public async Task Handle( lastName: request.UserSurname, email: request.UserEmail, phoneNumber: request.PhoneNumber ?? string.Empty, - adress: request.Address ?? string.Empty + address: request.Address ?? string.Empty ); var createResult = await _userManager.CreateAsync(user, request.UserPassword); From 136f7509d2aa78e44ad784b315ab1abc54edeec5 Mon Sep 17 00:00:00 2001 From: saijaku0 Date: Wed, 18 Feb 2026 23:12:46 +0100 Subject: [PATCH 3/3] fix: coderabbit errors --- src/Booking/Booking.API/Program.cs | 3 ++- .../Common/Behaviors/AuthorizationBehavior.cs | 4 ++-- .../Common/Exceptions/ForbiddenAccessException.cs | 2 +- .../Booking.Application/Common/Security/AuthorizeAttribute.cs | 4 ++-- .../UpdateScheduleConfigCommandHandler.cs | 4 ++-- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Booking/Booking.API/Program.cs b/src/Booking/Booking.API/Program.cs index 79a242e..7cb59d7 100644 --- a/src/Booking/Booking.API/Program.cs +++ b/src/Booking/Booking.API/Program.cs @@ -43,7 +43,8 @@ cfg.RegisterServicesFromAssembly(typeof(CreateAppointmentCommand).Assembly); cfg.AddBehavior(typeof(IPipelineBehavior<,>)); - cfg.AddOpenBehaviors([typeof(ValidationBehavior<,>), typeof(AuthorizationBehavior<,>)]); + cfg.AddOpenBehavior(typeof(AuthorizationBehavior<,>)); + cfg.AddOpenBehavior(typeof(ValidationBehavior<,>)); }); builder.Services.AddCors(options => diff --git a/src/Booking/Booking.Application/Common/Behaviors/AuthorizationBehavior.cs b/src/Booking/Booking.Application/Common/Behaviors/AuthorizationBehavior.cs index e934428..98cd86a 100644 --- a/src/Booking/Booking.Application/Common/Behaviors/AuthorizationBehavior.cs +++ b/src/Booking/Booking.Application/Common/Behaviors/AuthorizationBehavior.cs @@ -1,6 +1,6 @@ -using Booking.Application.Common.Interfaces; +using Booking.Application.Common.Exceptions; +using Booking.Application.Common.Interfaces; using Booking.Application.Common.Security; -using Booking.Application.Doctors.Command.DeleteDoctor; using MediatR; using System.Reflection; diff --git a/src/Booking/Booking.Application/Common/Exceptions/ForbiddenAccessException.cs b/src/Booking/Booking.Application/Common/Exceptions/ForbiddenAccessException.cs index 7f8e532..9796128 100644 --- a/src/Booking/Booking.Application/Common/Exceptions/ForbiddenAccessException.cs +++ b/src/Booking/Booking.Application/Common/Exceptions/ForbiddenAccessException.cs @@ -1,4 +1,4 @@ -namespace Booking.Application.Doctors.Command.DeleteDoctor +namespace Booking.Application.Common.Exceptions { [Serializable] internal class ForbiddenAccessException : Exception diff --git a/src/Booking/Booking.Application/Common/Security/AuthorizeAttribute.cs b/src/Booking/Booking.Application/Common/Security/AuthorizeAttribute.cs index 5772f2d..1aba657 100644 --- a/src/Booking/Booking.Application/Common/Security/AuthorizeAttribute.cs +++ b/src/Booking/Booking.Application/Common/Security/AuthorizeAttribute.cs @@ -14,7 +14,7 @@ public string[]? Roles { } } - // Now it's useless but in future - public string Policy { get; set; } + //TO DO: Now it's useless but in future + public string? Policy { get; set; } } } diff --git a/src/Booking/Booking.Application/Doctors/Command/UpdateScheduleConfig/UpdateScheduleConfigCommandHandler.cs b/src/Booking/Booking.Application/Doctors/Command/UpdateScheduleConfig/UpdateScheduleConfigCommandHandler.cs index d89ef39..1cbfd27 100644 --- a/src/Booking/Booking.Application/Doctors/Command/UpdateScheduleConfig/UpdateScheduleConfigCommandHandler.cs +++ b/src/Booking/Booking.Application/Doctors/Command/UpdateScheduleConfig/UpdateScheduleConfigCommandHandler.cs @@ -1,5 +1,5 @@ -using Booking.Application.Common.Interfaces; -using Booking.Application.Doctors.Command.DeleteDoctor; +using Booking.Application.Common.Exceptions; +using Booking.Application.Common.Interfaces; using Booking.Domain.Entities; using MediatR; using Microsoft.EntityFrameworkCore;