Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Booking/Booking.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
{
cfg.RegisterServicesFromAssembly(typeof(CreateAppointmentCommand).Assembly);

cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>));
cfg.AddOpenBehavior(typeof(AuthorizationBehavior<,>));
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
});

builder.Services.AddCors(options =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Booking.Application.Common.Exceptions;
using Booking.Application.Common.Interfaces;
using Booking.Application.Common.Security;
using MediatR;
using System.Reflection;

namespace Booking.Application.Common.Behaviors
{
public class AuthorizationBehavior<TRequest, TResponse>(
ICurrentUserService userService,
IIdentityService identityService)
: IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
{
private readonly ICurrentUserService _userService = userService;
private readonly IIdentityService _identityService = identityService;

public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var authorizeAttributes = request
.GetType().GetCustomAttributes<AuthorizeAttribute>()
.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<bool> IsUserInAnyRoleAsync(
string userId,
IEnumerable<AuthorizeAttribute> attributesWithRoles)
{
foreach (var attr in attributesWithRoles)
foreach (var role in attr.Roles!)
if (await _identityService.IsInRoleAsync(userId, role.Trim()))
return true;

return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Booking.Application.Doctors.Command.DeleteDoctor
namespace Booking.Application.Common.Exceptions
{
[Serializable]
internal class ForbiddenAccessException : Exception
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ namespace Booking.Application.Common.Extension
{
public static class AppointmentQueryExtensions
{
/// <summary>
/// Filters appointments that overlap with the specified time interval.
/// </summary>
/// <param name="doctorId">Optional doctor ID to filter by. If null, all doctors are considered.</param>
/// <param name="start">Start of the interval (exclusive on the right).</param>
/// <param name="end">End of the interval (exclusive on the left).</param>
/// <returns>An IQueryable of appointments that overlap with the interval.</returns>
public static IQueryable<Appointment> WhereOverlaps(
this IQueryable<Appointment> query,
Guid? doctorId,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}

//TO DO: Now it's useless but in future
public string? Policy { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
using Booking.Application.Common.Interfaces;
using Booking.Application.Common.Exceptions;
using Booking.Application.Common.Interfaces;
using Booking.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace Booking.Application.Doctors.Command.UpdateScheduleConfig
{
public class UpdateScheduleConfigCommandHandler(
IBookingDbContext context) : IRequestHandler<UpdateScheduleConfigCommand, Unit>
IBookingDbContext context,
ICurrentUserService userService)
: IRequestHandler<UpdateScheduleConfigCommand, Unit>
{
private readonly IBookingDbContext _context = context;
public async Task<Unit> Handle(UpdateScheduleConfigCommand request, CancellationToken cancellationToken)
private readonly ICurrentUserService _userService = userService;
public async Task<Unit> 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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
string UserName,
string UserSurname,
string UserEmail,
string UserPassword,
DateOnly DateOfBirth,
Gender Gender,
string? PhoneNumber,
string? Address
) : IRequest<string>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public async Task<string> 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);
Expand Down
4 changes: 2 additions & 2 deletions src/Booking/Booking.Domain/Entities/ApplicationUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
public virtual Patient? PatientProfile { get; private set; }
public virtual Doctor? DoctorProfile { get; private set; }

private ApplicationUser() { }

Check warning on line 15 in src/Booking/Booking.Domain/Entities/ApplicationUser.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'LastName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in src/Booking/Booking.Domain/Entities/ApplicationUser.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'FirstName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in src/Booking/Booking.Domain/Entities/ApplicationUser.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'LastName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in src/Booking/Booking.Domain/Entities/ApplicationUser.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'FirstName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in src/Booking/Booking.Domain/Entities/ApplicationUser.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'LastName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in src/Booking/Booking.Domain/Entities/ApplicationUser.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'FirstName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in src/Booking/Booking.Domain/Entities/ApplicationUser.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'LastName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in src/Booking/Booking.Domain/Entities/ApplicationUser.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'FirstName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

public ApplicationUser(
string firstName,
Expand Down Expand Up @@ -61,7 +61,7 @@
string lastName,
string email,
string phoneNumber,
string adress)
string address)
{
return new ApplicationUser
{
Expand All @@ -70,7 +70,7 @@
FirstName = firstName,
LastName = lastName,
PhoneNumber = phoneNumber,
Address = adress,
Address = address,
EmailConfirmed = true
};
}
Expand Down
Loading