From c6f7b7be4155fa1b52b9da16ac0a6634a7683c5e Mon Sep 17 00:00:00 2001 From: Nima Haji Date: Sun, 17 May 2026 14:07:04 +0330 Subject: [PATCH] Refactor Domain (DDD) --- .idea/.idea.Reservation_System/.idea/.name | 1 + Application/Application.csproj | 5 + .../Common/Interfaces/IJwtTokenGenerator.cs | 1 + .../{IUSerContext.cs => IUserContext.cs} | 2 +- .../Interfaces/IAppointmentRepository.cs | 14 --- .../Interfaces/IAppointmentService.cs | 1 + .../Services/AppointmentService.cs | 8 +- .../Auth/Interfaces/IUserRepository.cs | 14 --- .../Auth/Services/PasswordRecoveryService.cs | 9 +- .../Features/Auth/Services/UserService.cs | 6 +- .../Service/Interfaces/IServiceRepository.cs | 13 --- .../Service/Services/ServiceAppService.cs | 8 +- Domain/Aggregates/Appointment/Appointment.cs | 64 +++++++++++++ Domain/Aggregates/Service/Service.cs | 32 +++++++ Domain/Aggregates/User/User.cs | 95 +++++++++++++++++++ Domain/Base/AggregatedRoot.cs | 11 +++ Domain/Base/DomainEvent.cs | 6 ++ Domain/Base/EntityBase.cs | 6 ++ Domain/Entities/Appointment.cs | 61 ------------ Domain/Entities/AppointmentServiceLink.cs | 17 ---- Domain/Entities/Refreshtoken.cs | 22 +++-- Domain/Entities/Service.cs | 27 ------ Domain/Entities/User.cs | 71 -------------- .../Appointment/AppointmentCacelledEvent.cs | 8 ++ .../Appointment/AppointmentCompletedEvent.cs | 8 ++ .../Appointment/AppointmentCreatedEvent.cs | 8 ++ .../AppointmentRescheduledEvent.cs | 8 ++ Domain/Events/Service/ServiceCreatedEvent.cs | 8 ++ Domain/Events/Service/ServiceUpdatedEvent.cs | 8 ++ Domain/Events/User/UserProfileUpdatedEvent.cs | 7 ++ Domain/Events/User/UserRegisteredEvent.cs | 7 ++ Domain/Events/User/UserRemovedEvent.cs | 6 ++ Domain/Repository/IAppointmentRepository.cs | 12 +++ Domain/Repository/IServiceRepository.cs | 12 +++ Domain/Repository/IUserRepository.cs | 12 +++ .../IappointmenServiceLinkRepository.cs | 2 +- Domain/ValueObject/AppointmentTimeRange.cs | 17 ++++ Domain/ValueObject/Email.cs | 36 +++++++ Domain/ValueObject/Password.cs | 37 ++++++++ Domain/ValueObject/PhoneNumber.cs | 26 +++++ Infrastructure/DependencyInjection.cs | 5 +- Infrastructure/Persistance/AppDbContext.cs | 3 + .../AppointmentConfiguration.cs | 1 + .../Configurations/ServiceConfiguration.cs | 1 + .../Configurations/UserConfiguration.cs | 1 + Infrastructure/Persistance/DbInitializer.cs | 1 + .../Repositories/AppointmentRepository.cs | 2 + .../AppointmentServiceLinkRepository.cs | 2 +- .../Repositories/ServiceRepository.cs | 3 +- .../Repositories/UserRepository.cs | 2 + .../Security/Context/UserContext.cs | 2 +- .../Security/Jwt/JwtTokenService.cs | 3 +- api/Controllers/UserController.cs | 1 + api/Program.cs | 3 +- 54 files changed, 493 insertions(+), 253 deletions(-) create mode 100644 .idea/.idea.Reservation_System/.idea/.name rename Application/Common/Interfaces/{IUSerContext.cs => IUserContext.cs} (85%) delete mode 100644 Application/Features/Appointments/Interfaces/IAppointmentRepository.cs delete mode 100644 Application/Features/Auth/Interfaces/IUserRepository.cs delete mode 100644 Application/Features/Service/Interfaces/IServiceRepository.cs create mode 100644 Domain/Aggregates/Appointment/Appointment.cs create mode 100644 Domain/Aggregates/Service/Service.cs create mode 100644 Domain/Aggregates/User/User.cs create mode 100644 Domain/Base/AggregatedRoot.cs create mode 100644 Domain/Base/DomainEvent.cs create mode 100644 Domain/Base/EntityBase.cs delete mode 100644 Domain/Entities/Appointment.cs delete mode 100644 Domain/Entities/AppointmentServiceLink.cs delete mode 100644 Domain/Entities/Service.cs delete mode 100644 Domain/Entities/User.cs create mode 100644 Domain/Events/Appointment/AppointmentCacelledEvent.cs create mode 100644 Domain/Events/Appointment/AppointmentCompletedEvent.cs create mode 100644 Domain/Events/Appointment/AppointmentCreatedEvent.cs create mode 100644 Domain/Events/Appointment/AppointmentRescheduledEvent.cs create mode 100644 Domain/Events/Service/ServiceCreatedEvent.cs create mode 100644 Domain/Events/Service/ServiceUpdatedEvent.cs create mode 100644 Domain/Events/User/UserProfileUpdatedEvent.cs create mode 100644 Domain/Events/User/UserRegisteredEvent.cs create mode 100644 Domain/Events/User/UserRemovedEvent.cs create mode 100644 Domain/Repository/IAppointmentRepository.cs create mode 100644 Domain/Repository/IServiceRepository.cs create mode 100644 Domain/Repository/IUserRepository.cs rename {Application/Features/AppointmentServiceLink/Interfaces => Domain/Repository}/IappointmenServiceLinkRepository.cs (81%) create mode 100644 Domain/ValueObject/AppointmentTimeRange.cs create mode 100644 Domain/ValueObject/Email.cs create mode 100644 Domain/ValueObject/Password.cs create mode 100644 Domain/ValueObject/PhoneNumber.cs diff --git a/.idea/.idea.Reservation_System/.idea/.name b/.idea/.idea.Reservation_System/.idea/.name new file mode 100644 index 0000000..4317306 --- /dev/null +++ b/.idea/.idea.Reservation_System/.idea/.name @@ -0,0 +1 @@ +Reservation_System \ No newline at end of file diff --git a/Application/Application.csproj b/Application/Application.csproj index 990d47f..a79ac23 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -18,4 +18,9 @@ + + + + + diff --git a/Application/Common/Interfaces/IJwtTokenGenerator.cs b/Application/Common/Interfaces/IJwtTokenGenerator.cs index d6fca23..6cebaf0 100644 --- a/Application/Common/Interfaces/IJwtTokenGenerator.cs +++ b/Application/Common/Interfaces/IJwtTokenGenerator.cs @@ -1,3 +1,4 @@ +using Domain.Aggregates.User; using Domain.Entities; namespace Application.Common.Interfaces; diff --git a/Application/Common/Interfaces/IUSerContext.cs b/Application/Common/Interfaces/IUserContext.cs similarity index 85% rename from Application/Common/Interfaces/IUSerContext.cs rename to Application/Common/Interfaces/IUserContext.cs index 50ab73e..3c67a1e 100644 --- a/Application/Common/Interfaces/IUSerContext.cs +++ b/Application/Common/Interfaces/IUserContext.cs @@ -2,7 +2,7 @@ namespace Application.Common.Interfaces; -public interface IUSerContext +public interface IUserContext { Guid UserId { get;} string? Email { get;} diff --git a/Application/Features/Appointments/Interfaces/IAppointmentRepository.cs b/Application/Features/Appointments/Interfaces/IAppointmentRepository.cs deleted file mode 100644 index 9fb57b7..0000000 --- a/Application/Features/Appointments/Interfaces/IAppointmentRepository.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Application.Features.Appointments.DTOs; -using Domain.Entities; - -namespace Application.Features.Appointments.Interfaces; - -public interface IAppointmentRepository -{ - Task AddAsync(Appointment appointment); - Task SaveAsync(); - Task> ViewAppointments(); - Task> ViewAppointments(Guid userId); - Task GetAppointmentByIdAsync(Guid appointmentId); - Task IsExistBy(Guid appointmentId); -} \ No newline at end of file diff --git a/Application/Features/Appointments/Interfaces/IAppointmentService.cs b/Application/Features/Appointments/Interfaces/IAppointmentService.cs index 3fa756f..521acb0 100644 --- a/Application/Features/Appointments/Interfaces/IAppointmentService.cs +++ b/Application/Features/Appointments/Interfaces/IAppointmentService.cs @@ -1,4 +1,5 @@ using Application.Features.Appointments.DTOs; +using Domain.Aggregates.Appointment; using Domain.Entities; namespace Application.Features.Appointments.Interfaces; diff --git a/Application/Features/Appointments/Services/AppointmentService.cs b/Application/Features/Appointments/Services/AppointmentService.cs index 9501aef..2a50023 100644 --- a/Application/Features/Appointments/Services/AppointmentService.cs +++ b/Application/Features/Appointments/Services/AppointmentService.cs @@ -1,12 +1,12 @@ using Application.Common.Interfaces; using Application.Features.Appointments.DTOs; using Application.Features.Appointments.Interfaces; -using Application.Features.AppointmentServiceLink.Interfaces; using Application.Features.Auth.Interfaces; -using Application.Features.Service.Interfaces; +using Domain.Aggregates.Appointment; using Domain.Entities; using Domain.Enums; using Domain.Exceptions; +using Domain.Repository; namespace Application.Features.Appointments.Services; @@ -16,11 +16,11 @@ public class AppointmentService : IAppointmentService private readonly IUserRepository _userRepository; private readonly IServiceRepository _serviceRepository; private readonly IAppointmenServiceLinkRepository _appointmenServiceLinkRepository; - private readonly IUSerContext _userContext; + private readonly IUserContext _userContext; public AppointmentService(IAppointmentRepository repository, IUserRepository userRepository, IServiceRepository serviceRepository, IAppointmenServiceLinkRepository appointmenServiceLinkRepository, - IUSerContext userContext) + IUserContext userContext) { _appointmentRepository = repository; _userRepository = userRepository; diff --git a/Application/Features/Auth/Interfaces/IUserRepository.cs b/Application/Features/Auth/Interfaces/IUserRepository.cs deleted file mode 100644 index 37a16a5..0000000 --- a/Application/Features/Auth/Interfaces/IUserRepository.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Application.Features.Auth.DTOs; -using Domain.Entities; - -namespace Application.Features.Auth.Interfaces; - -public interface IUserRepository -{ - Task RegisterUserAsync(User user); - Task GetUserByEmailAsync(string email); - Task IsUserExistsByIdAsync(Guid userId); - Task SaveChangesAsync(); - Task> GetAllUsersAsync(); - Task GetUserByIdAsync(Guid userId); -} \ No newline at end of file diff --git a/Application/Features/Auth/Services/PasswordRecoveryService.cs b/Application/Features/Auth/Services/PasswordRecoveryService.cs index 6127357..3b5e442 100644 --- a/Application/Features/Auth/Services/PasswordRecoveryService.cs +++ b/Application/Features/Auth/Services/PasswordRecoveryService.cs @@ -2,6 +2,7 @@ using Application.Features.Auth.Interfaces; using Domain.Entities; using Domain.Exceptions; +using Domain.Repository; namespace Application.Features.Auth.Services; @@ -55,13 +56,5 @@ public async Task ForgetPasswordAsync(string email) ); await _userRepository.SaveChangesAsync(); - - //Implement Email Service - - // await _emailService.SendAsync( - // email, - // "Password Reset Code", - // $"Your verification code is: {code}" - // ); } } \ No newline at end of file diff --git a/Application/Features/Auth/Services/UserService.cs b/Application/Features/Auth/Services/UserService.cs index ccdcca1..8c8ad0f 100644 --- a/Application/Features/Auth/Services/UserService.cs +++ b/Application/Features/Auth/Services/UserService.cs @@ -3,7 +3,9 @@ using Application.Common.Interfaces.Repositories; using Application.Features.Auth.DTOs; using Application.Features.Auth.Interfaces; +using Domain.Aggregates.User; using Domain.Entities; +using Domain.Repository; namespace Application.Features.Auth.Services; @@ -14,12 +16,12 @@ public class UserService : IUserService private readonly IPasswordHasher _passwordHasher; private readonly IJwtTokenService _jwtTokenService; private readonly IRefreshTokenRepository _refreshTokenRepository; - private readonly IUSerContext _userContext; + private readonly IUserContext _userContext; private readonly IVerificationCodeGenerator _verificationCodeGenerator; public UserService(IUserRepository repository, IJwtTokenService jwtTokenGenerator, IRefreshTokenRepository refreshTokenRepository, IHasher hasher, IPasswordHasher passwordHasher, - IUSerContext userContext, IVerificationCodeGenerator verificationCodeGenerator) + IUserContext userContext, IVerificationCodeGenerator verificationCodeGenerator) { _userRepository = repository; _jwtTokenService = jwtTokenGenerator; diff --git a/Application/Features/Service/Interfaces/IServiceRepository.cs b/Application/Features/Service/Interfaces/IServiceRepository.cs deleted file mode 100644 index 379feef..0000000 --- a/Application/Features/Service/Interfaces/IServiceRepository.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Application.Features.Service.DTOs; - -namespace Application.Features.Service.Interfaces; - -public interface IServiceRepository -{ - Task CreatServiceAsync(Domain.Entities.Service service); - Task SaveChangesAsync(); - Task DeleteServiceAsync(Guid serviceId); - Task> ViewAllServiceAsync(); - Task GetServiceByIdAsync(Guid serviceId); - Task> GetServiceListByIdsAsync(List serviceIds); -} \ No newline at end of file diff --git a/Application/Features/Service/Services/ServiceAppService.cs b/Application/Features/Service/Services/ServiceAppService.cs index d9fbc74..7c2dbca 100644 --- a/Application/Features/Service/Services/ServiceAppService.cs +++ b/Application/Features/Service/Services/ServiceAppService.cs @@ -1,16 +1,16 @@ using Application.Common.Interfaces; using Application.Features.Service.DTOs; -using Application.Features.Service.Interfaces; using Domain.Enums; using Domain.Exceptions; +using Domain.Repository; namespace Application.Features.Service.Services; public class ServiceAppService:IServiceAppService { private readonly IServiceRepository _repository; - private readonly IUSerContext _userContext; - public ServiceAppService(IServiceRepository repository, IUSerContext context) + private readonly IUserContext _userContext; + public ServiceAppService(IServiceRepository repository, IUserContext context) { _repository = repository; _userContext = context; @@ -24,7 +24,7 @@ private void EnsureAdmin() public async Task CreateServiceAsync(CreateService createService) { EnsureAdmin(); - var service=new Domain.Entities.Service(createService.Title,createService.DurationMinutes); + var service=new Domain.Aggregates.Service.Service(createService.Title,createService.DurationMinutes); await _repository.CreatServiceAsync(service); return $"{service.Title} created"; } diff --git a/Domain/Aggregates/Appointment/Appointment.cs b/Domain/Aggregates/Appointment/Appointment.cs new file mode 100644 index 0000000..398beea --- /dev/null +++ b/Domain/Aggregates/Appointment/Appointment.cs @@ -0,0 +1,64 @@ +using Domain.Base; +using Domain.Enums; +using Domain.ValueObject; + +namespace Domain.Aggregates.Appointment +{ + public class Appointment:AggregatedRoot + { + public Guid AppointmentId { get; private set; } + public Guid UserId { get; private set; } + public string Title { get; private set; } + public AppointmentTimeRange TimeRange { get; private set; } + public AppointmentStatus Status { get; private set; } + + private readonly List _serviceIds = new(); + public IReadOnlyList ServiceIds=>_serviceIds; + + public void AddServices(IEnumerable serviceIds) + { + foreach (var id in serviceIds.Distinct()) + { + if (!_serviceIds.Contains(id)) + _serviceIds.Add(id); + } + } + + private Appointment() { } // For EF + + public Appointment(Guid userId,AppointmentTimeRange timeRange, string title) + { + if (string.IsNullOrEmpty(title)) + throw new ArgumentException("Title cannot be null or empty"); + AppointmentId = Guid.NewGuid(); + UserId = userId; + TimeRange = timeRange??throw new ArgumentNullException(nameof(timeRange)) ; + Title = title; + Status = AppointmentStatus.Reserved; + } + + public void Cancel() + { + if (Status == AppointmentStatus.Cancelled) + throw new InvalidOperationException("Already cancelled"); + + Status = AppointmentStatus.Cancelled; + } + + public void Reschedule(AppointmentTimeRange timeRange) + { + if (Status == AppointmentStatus.Cancelled) + throw new InvalidOperationException("Cannot reschedule cancelled appointment"); + TimeRange = timeRange ?? throw new ArgumentNullException(nameof(timeRange)); + } + public void ChangeTitle(string title) + { + if (Status == AppointmentStatus.Cancelled) + throw new InvalidOperationException("Cannot edit cancelled appointment"); + + if (string.IsNullOrEmpty(title)) + throw new ArgumentException("Title cannot be null or empty"); + Title=title; + } + } +} diff --git a/Domain/Aggregates/Service/Service.cs b/Domain/Aggregates/Service/Service.cs new file mode 100644 index 0000000..6e15af6 --- /dev/null +++ b/Domain/Aggregates/Service/Service.cs @@ -0,0 +1,32 @@ +using Domain.Base; + +namespace Domain.Aggregates.Service; + +public class Service:AggregatedRoot +{ + public Guid Id { get; private set; } + public string Title { get; private set; } + public TimeSpan DurationMinutes { get; private set; } + private Service() { } + + public Service(string title, TimeSpan durationMinutes) + { + if (string.IsNullOrEmpty(title)) + throw new ArgumentException("Title required"); + if (durationMinutes.TotalMinutes <= 0) + throw new ArgumentException("Invalid duration"); + Id = Guid.NewGuid(); + Title = title; + DurationMinutes = durationMinutes; + } + + public void Edit(TimeSpan durationMinutes,string title) + { + if (string.IsNullOrEmpty(title)) + throw new ArgumentException("Title required"); + if (durationMinutes.TotalMinutes <= 0) + throw new ArgumentException("Invalid duration"); + DurationMinutes = durationMinutes; + Title = title; + } +} \ No newline at end of file diff --git a/Domain/Aggregates/User/User.cs b/Domain/Aggregates/User/User.cs new file mode 100644 index 0000000..69cf40a --- /dev/null +++ b/Domain/Aggregates/User/User.cs @@ -0,0 +1,95 @@ +using Domain.Base; +using Domain.Entities; +using Domain.Enums; +using Domain.ValueObject; + +namespace Domain.Aggregates.User; + +public class User:AggregatedRoot +{ + public Guid Id { get; private set; } + public string FullName { get; private set; } + public UserRole Role { get; private set; } + public Email Email { get; private set; } + public PhoneNumber PhoneNumber { get; private set; } + public Password Password { get; private set; } + public string? PasswordResetCodeHash { get; private set; } + public DateTime? PasswordResetCodeExpireAt { get; private set; } + public int PasswordResetAttemptsCount { get; private set; } + private readonly List _refreshTokens = new(); + public IReadOnlyList RefreshTokens => _refreshTokens; + + public void AddRefreshToken(string refreshToken, DateTime expiresAt) + { + _refreshTokens.RemoveAll(tk => tk.IsExpired); + _refreshTokens.Add(new RefreshToken(refreshToken, expiresAt)); + } + + public void RevokeRefreshToken(Guid tokenId) + { + var token = _refreshTokens.FirstOrDefault(tk => tk.Id == tokenId); + if (token == null) throw new InvalidOperationException("Refresh token not found"); + token.Revoke(); + } + + private User() + { + } + + public User(string fullName, Email email, PhoneNumber phoneNumber, Password password) + { + Id = Guid.NewGuid(); + FullName = fullName; + Role = UserRole.User; + Email = email; + PhoneNumber = phoneNumber; + Password = password; + } + + public User(string fullName, Email email, PhoneNumber phoneNumber, UserRole role, Password password) + { + Id = Guid.NewGuid(); + FullName = fullName; + Role = role; + Email = email ?? throw new ArgumentNullException(nameof(email)); + PhoneNumber = phoneNumber ?? throw new ArgumentNullException(nameof(phoneNumber)); + Password = password ?? throw new ArgumentNullException(nameof(password)); + } + + public void UpdateProfile(string fullName, PhoneNumber phoneNumber) + { + if (string.IsNullOrWhiteSpace(fullName)) throw new ArgumentException("Full name required"); + if (phoneNumber == null) throw new ArgumentException("Phone number required"); + FullName = fullName; + PhoneNumber = phoneNumber; + } + + public void ResetPassword(string codeHash, DateTime expiresAt) + { + if (string.IsNullOrWhiteSpace(codeHash)) throw new ArgumentException("Code hash required"); + if (expiresAt <= DateTime.UtcNow) throw new ArgumentException("Expires must be in future"); + PasswordResetCodeHash = codeHash; + PasswordResetCodeExpireAt = expiresAt; + PasswordResetAttemptsCount = 0; + } + + private const int MaxResetAttempts = 5; + + public bool CanUseResetPassword(string codeHash, DateTime now) + { + if (PasswordResetCodeExpireAt == null || now > PasswordResetCodeExpireAt) + return false; + if (PasswordResetAttemptsCount >= MaxResetAttempts) + return false; + return PasswordResetCodeHash == codeHash; + } + + public void IncreasePasswordResetAttemptCount() => PasswordResetAttemptsCount++; + + public void ClearPasswordResetCode() + { + PasswordResetCodeHash = null; + PasswordResetCodeExpireAt = null; + PasswordResetAttemptsCount = 0; + } +} \ No newline at end of file diff --git a/Domain/Base/AggregatedRoot.cs b/Domain/Base/AggregatedRoot.cs new file mode 100644 index 0000000..3ab8ce9 --- /dev/null +++ b/Domain/Base/AggregatedRoot.cs @@ -0,0 +1,11 @@ +namespace Domain.Base; + +public class AggregatedRoot:EntityBase +{ + private readonly List _domainEvents=[]; + public IReadOnlyCollection DomainEvents => _domainEvents; + + protected void AddDomainEvent(DomainEvent domainEvent)=> _domainEvents.Add(domainEvent); + + public void ClearDomainEvents() => _domainEvents.Clear(); +} \ No newline at end of file diff --git a/Domain/Base/DomainEvent.cs b/Domain/Base/DomainEvent.cs new file mode 100644 index 0000000..0d3dc1e --- /dev/null +++ b/Domain/Base/DomainEvent.cs @@ -0,0 +1,6 @@ +namespace Domain.Base; + +public abstract record DomainEvent +{ + public DateTime OccurredOn { get; init; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/Domain/Base/EntityBase.cs b/Domain/Base/EntityBase.cs new file mode 100644 index 0000000..e73f003 --- /dev/null +++ b/Domain/Base/EntityBase.cs @@ -0,0 +1,6 @@ +namespace Domain.Base; + +public class EntityBase +{ + public Guid Id { get; protected set; } +} \ No newline at end of file diff --git a/Domain/Entities/Appointment.cs b/Domain/Entities/Appointment.cs deleted file mode 100644 index f2852e1..0000000 --- a/Domain/Entities/Appointment.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Domain.Enums; - -namespace Domain.Entities -{ - public class Appointment - { - public Guid AppointmentId { get; private set; } - public Guid UserId { get; private set; } - public User User { get; private set; } = null!; - public string Title { get; private set; } - public DateTime StartTime { get; private set; } - public DateTime EndTime { get; private set; } - public AppointmentStatus Status { get; private set; } - - public ICollection AppointmentServices { get; private set; } = new List(); - - private Appointment() { } // For EF - - public Appointment(Guid userId, DateTime startTime, DateTime endTime, string title) - { - if (startTime == default || endTime == default) - throw new ArgumentException("StartTime and EndTime are required"); - if (endTime <= startTime) - throw new ArgumentException("EndTime must be after StartTime"); - - AppointmentId = Guid.NewGuid(); - UserId = userId; - StartTime = startTime; - EndTime = endTime; - Title = title; - Status = AppointmentStatus.Reserved; - } - - public void AddServices(IEnumerable services) - { - foreach (var service in services) - { - AppointmentServices.Add(new AppointmentServiceLink(AppointmentId, service.Id)); - } - } - - public void Cancel() - { - if (Status == AppointmentStatus.Cancelled) - throw new InvalidOperationException("Already cancelled"); - - Status = AppointmentStatus.Cancelled; - } - - public void Edit(DateTime startTime, DateTime endTime) - { - if (startTime == default || endTime == default) - throw new ArgumentException("StartTime and EndTime are required"); - if (endTime <= startTime) - throw new ArgumentException("EndTime must be after StartTime"); - - StartTime = startTime; - EndTime = endTime; - } - } -} diff --git a/Domain/Entities/AppointmentServiceLink.cs b/Domain/Entities/AppointmentServiceLink.cs deleted file mode 100644 index df95475..0000000 --- a/Domain/Entities/AppointmentServiceLink.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Domain.Entities; - -public class AppointmentServiceLink -{ - public Guid AppointmentId { get; set; } - public Appointment Appointment { get; set; } - public Guid ServiceId { get; set; } - public Service Service { get; set; } - - private AppointmentServiceLink() { } - - public AppointmentServiceLink(Guid appointmentId, Guid serviceId) - { - AppointmentId = appointmentId; - ServiceId = serviceId; - } -} \ No newline at end of file diff --git a/Domain/Entities/Refreshtoken.cs b/Domain/Entities/Refreshtoken.cs index d198a14..6e712a3 100644 --- a/Domain/Entities/Refreshtoken.cs +++ b/Domain/Entities/Refreshtoken.cs @@ -2,12 +2,20 @@ namespace Domain.Entities; public class RefreshToken { - public Guid Id { get; set; } + public Guid Id { get; private set; } - public string Token { get; set; } = null!; - public DateTime ExpiresAt { get; set; } - public bool IsRevoked { get; set; } + public string Token { get; private set; } = null!; + public DateTime ExpiresAt { get; private set; } + public bool IsRevoked { get; private set; } + public bool IsExpired => ExpiresAt < DateTime.UtcNow; + public bool IsActive => !IsExpired && !IsRevoked; - public Guid UserId { get; set; } - public User User { get; set; } = null!; -} + internal RefreshToken(string token, DateTime expiresAt) + { + Id = Guid.NewGuid(); + Token = token ?? throw new ArgumentNullException(nameof(token)); + ExpiresAt = expiresAt; + IsRevoked = false; + } + public void Revoke()=> IsRevoked = true; +} \ No newline at end of file diff --git a/Domain/Entities/Service.cs b/Domain/Entities/Service.cs deleted file mode 100644 index 9aca70f..0000000 --- a/Domain/Entities/Service.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Domain.Entities; - -public class Service -{ - public Guid Id { get; private set; } - public string Title { get; private set; } - public int DurationMinutes { get; private set; } - public ICollection AppointmentServices { get; set; }=new List(); - - private Service() { } - - public Service(string title, int durationMinutes) - { - if (durationMinutes <= 0) - throw new ArgumentException("Invalid duration"); - - Id = Guid.NewGuid(); - Title = title; - DurationMinutes = durationMinutes; - } - - public void Edit(int durationMinutes,string title) - { - DurationMinutes = durationMinutes; - Title = title; - } -} \ No newline at end of file diff --git a/Domain/Entities/User.cs b/Domain/Entities/User.cs deleted file mode 100644 index 70602ec..0000000 --- a/Domain/Entities/User.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Domain.Enums; - -namespace Domain.Entities; - -public class User -{ - public Guid Id { get; private set; } - public string FullName { get; private set; } - public UserRole Role { get; private set; } - public string Email { get; private set; } - public string PhoneNumber { get; private set; } - public string Password { get; private set; } - public string? PasswordResetCodeHash { get; private set; } - public DateTime? PasswordResetCodeExpireAt { get; private set; } - public int PasswordResetAttemptsCount { get; private set; } - public ICollection RefreshTokens { get; private set; } = new List(); - public ICollection Appointments { get; private set; } = new List(); - - public User() - { - } - - public User(string fullName, string email, string phoneNumber, string password) - { - Id = Guid.NewGuid(); - FullName = fullName; - Role = UserRole.User; - Email = email; - PhoneNumber = phoneNumber; - Password = password; - } - - public User(string fullName, string email, string phoneNumber, UserRole role, string password) - { - Id = Guid.NewGuid(); - FullName = fullName; - Role = role; - Email = email; - PhoneNumber = phoneNumber; - Password = password; - } - - public void UpdateProfile(string fullName, string phoneNumber) - { - FullName = fullName; - PhoneNumber = phoneNumber; - } - - public void ResetPassword(string codeHash, DateTime expiresAt) - { - PasswordResetCodeHash = codeHash; - PasswordResetCodeExpireAt = expiresAt; - PasswordResetAttemptsCount = 0; - } - - public bool CanUseResetPassword(string codeHash, DateTime now) - { - if (PasswordResetCodeExpireAt == null || now > PasswordResetCodeExpireAt) - return false; - - return PasswordResetCodeHash == codeHash; - } - public void IncreasePasswordResetAttemptCount()=>PasswordResetAttemptsCount++; - - public void ClearPasswordResetCode() - { - PasswordResetCodeHash = null; - PasswordResetCodeExpireAt = null; - PasswordResetAttemptsCount = 0; - } -} \ No newline at end of file diff --git a/Domain/Events/Appointment/AppointmentCacelledEvent.cs b/Domain/Events/Appointment/AppointmentCacelledEvent.cs new file mode 100644 index 0000000..2d6f696 --- /dev/null +++ b/Domain/Events/Appointment/AppointmentCacelledEvent.cs @@ -0,0 +1,8 @@ +using Domain.Base; + +namespace Domain.Events.Appointment; + +public record AppointmentCancelledEvent( + Guid AppointmentId, + Guid ServiceId, + Guid UserId) : DomainEvent; \ No newline at end of file diff --git a/Domain/Events/Appointment/AppointmentCompletedEvent.cs b/Domain/Events/Appointment/AppointmentCompletedEvent.cs new file mode 100644 index 0000000..57bc879 --- /dev/null +++ b/Domain/Events/Appointment/AppointmentCompletedEvent.cs @@ -0,0 +1,8 @@ +using Domain.Base; + +namespace Domain.Events.Appointment; + +public record AppointmentCompletedEvent( + Guid AppointmentId, + Guid ServiceId, + Guid UserId) : DomainEvent; \ No newline at end of file diff --git a/Domain/Events/Appointment/AppointmentCreatedEvent.cs b/Domain/Events/Appointment/AppointmentCreatedEvent.cs new file mode 100644 index 0000000..f21087d --- /dev/null +++ b/Domain/Events/Appointment/AppointmentCreatedEvent.cs @@ -0,0 +1,8 @@ +using Domain.Base; + +namespace Domain.Events.Appointment; + +public record AppointmentCreatedEvent( + Guid AppointmentId, + Guid ServiceId, + Guid UserId) : DomainEvent; \ No newline at end of file diff --git a/Domain/Events/Appointment/AppointmentRescheduledEvent.cs b/Domain/Events/Appointment/AppointmentRescheduledEvent.cs new file mode 100644 index 0000000..1f58df7 --- /dev/null +++ b/Domain/Events/Appointment/AppointmentRescheduledEvent.cs @@ -0,0 +1,8 @@ +using Domain.Base; + +namespace Domain.Events.Appointment; + +public record AppointmentRescheduledEvent( + Guid AppointmentId, + Guid ServiceId, + Guid UserId) : DomainEvent; \ No newline at end of file diff --git a/Domain/Events/Service/ServiceCreatedEvent.cs b/Domain/Events/Service/ServiceCreatedEvent.cs new file mode 100644 index 0000000..be1f69d --- /dev/null +++ b/Domain/Events/Service/ServiceCreatedEvent.cs @@ -0,0 +1,8 @@ +using Domain.Base; + +namespace Domain.Events.Service; + +public record ServiceCreatedEvent( + Guid ServiceId, + string ServiceName, + TimeSpan DurationTime):DomainEvent; \ No newline at end of file diff --git a/Domain/Events/Service/ServiceUpdatedEvent.cs b/Domain/Events/Service/ServiceUpdatedEvent.cs new file mode 100644 index 0000000..e40b1a4 --- /dev/null +++ b/Domain/Events/Service/ServiceUpdatedEvent.cs @@ -0,0 +1,8 @@ +using Domain.Base; + +namespace Domain.Events.Service; + +public record ServiceUpdatedEvent( + Guid ServiceId, + string ServiceName, + TimeSpan DurationTime):DomainEvent; \ No newline at end of file diff --git a/Domain/Events/User/UserProfileUpdatedEvent.cs b/Domain/Events/User/UserProfileUpdatedEvent.cs new file mode 100644 index 0000000..aab158e --- /dev/null +++ b/Domain/Events/User/UserProfileUpdatedEvent.cs @@ -0,0 +1,7 @@ +using Domain.Base; + +namespace Domain.Events.User; + +public record UserProfileUpdatedEvent( + Guid UserId, + string NewFullName):DomainEvent; \ No newline at end of file diff --git a/Domain/Events/User/UserRegisteredEvent.cs b/Domain/Events/User/UserRegisteredEvent.cs new file mode 100644 index 0000000..feaf741 --- /dev/null +++ b/Domain/Events/User/UserRegisteredEvent.cs @@ -0,0 +1,7 @@ +using Domain.Base; + +namespace Domain.Events.User; + +public record UserRegisteredEvent( + Guid UserId, + string Email):DomainEvent; \ No newline at end of file diff --git a/Domain/Events/User/UserRemovedEvent.cs b/Domain/Events/User/UserRemovedEvent.cs new file mode 100644 index 0000000..abc933b --- /dev/null +++ b/Domain/Events/User/UserRemovedEvent.cs @@ -0,0 +1,6 @@ +using Domain.Base; + +namespace Domain.Events.User; + +public record UserRemovedEvent( + Guid UserId):DomainEvent; \ No newline at end of file diff --git a/Domain/Repository/IAppointmentRepository.cs b/Domain/Repository/IAppointmentRepository.cs new file mode 100644 index 0000000..5cd28b7 --- /dev/null +++ b/Domain/Repository/IAppointmentRepository.cs @@ -0,0 +1,12 @@ +using Domain.Aggregates.Appointment; + +namespace Domain.Repository; + +public interface IAppointmentRepository +{ + Task AddAppointmentAsync(Appointment appointment); + Task> ViewAppointments(); + Task> ViewAppointments(Guid userId); + Task GetAppointmentByIdAsync(Guid appointmentId); + Task IsExistBy(Guid appointmentId); +} \ No newline at end of file diff --git a/Domain/Repository/IServiceRepository.cs b/Domain/Repository/IServiceRepository.cs new file mode 100644 index 0000000..37de629 --- /dev/null +++ b/Domain/Repository/IServiceRepository.cs @@ -0,0 +1,12 @@ +using Domain.Aggregates.Service; + +namespace Domain.Repository; + +public interface IServiceRepository +{ + Task AddServiceAsync(Service service); + Task DeleteServiceAsync(Guid serviceId); + Task> ViewAllServiceAsync(); + Task GetServiceByIdAsync(Guid serviceId); + Task> GetServiceListByIdsAsync(List serviceIds); +} \ No newline at end of file diff --git a/Domain/Repository/IUserRepository.cs b/Domain/Repository/IUserRepository.cs new file mode 100644 index 0000000..124029b --- /dev/null +++ b/Domain/Repository/IUserRepository.cs @@ -0,0 +1,12 @@ +using Domain.Aggregates.User; + +namespace Domain.Repository; + +public interface IUserRepository +{ + Task AddUserAsync(User user); + Task GetUserByEmailAsync(string email); + Task IsUserExistsByIdAsync(Guid userId); + Task> GetAllUsersAsync(); + Task GetUserByIdAsync(Guid userId); +} \ No newline at end of file diff --git a/Application/Features/AppointmentServiceLink/Interfaces/IappointmenServiceLinkRepository.cs b/Domain/Repository/IappointmenServiceLinkRepository.cs similarity index 81% rename from Application/Features/AppointmentServiceLink/Interfaces/IappointmenServiceLinkRepository.cs rename to Domain/Repository/IappointmenServiceLinkRepository.cs index 103cf74..e27cdf3 100644 --- a/Application/Features/AppointmentServiceLink/Interfaces/IappointmenServiceLinkRepository.cs +++ b/Domain/Repository/IappointmenServiceLinkRepository.cs @@ -1,4 +1,4 @@ -namespace Application.Features.AppointmentServiceLink.Interfaces; +namespace Domain.Repository; public interface IAppointmenServiceLinkRepository { diff --git a/Domain/ValueObject/AppointmentTimeRange.cs b/Domain/ValueObject/AppointmentTimeRange.cs new file mode 100644 index 0000000..15af97b --- /dev/null +++ b/Domain/ValueObject/AppointmentTimeRange.cs @@ -0,0 +1,17 @@ +namespace Domain.ValueObject; + +public class AppointmentTimeRange +{ + public DateTime StartTime { get; private set; } + public DateTime EndTime { get; private set; } + + public AppointmentTimeRange(DateTime startTime, DateTime endTime) + { + if (startTime == default || endTime == default) + throw new ArgumentException("Start and End are required"); + if(startTime >= endTime) + throw new ArgumentException("Start time must be before end time"); + StartTime = startTime; + EndTime = endTime; + } +} \ No newline at end of file diff --git a/Domain/ValueObject/Email.cs b/Domain/ValueObject/Email.cs new file mode 100644 index 0000000..f10888d --- /dev/null +++ b/Domain/ValueObject/Email.cs @@ -0,0 +1,36 @@ +using System.Text.RegularExpressions; + +namespace Domain.ValueObject; + +public class Email +{ + public string Value { get; private set; } + + private Email() + { + } + + public Email(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("Value cannot be null or whitespace."); + + if (!IsValidEmail(value)) + throw new ArgumentException("Value must be a valid email address."); + + Value = value.ToLowerInvariant(); + } + + private static bool IsValidEmail(string email) + { + var pattern = @"^[^@\s]+@[^@\s]+\.[^@\s]+$"; + return Regex.IsMatch(email, pattern, RegexOptions.IgnoreCase); + } + + public override bool Equals(object? obj) + { + if (obj is not Email other) return false; + return Value == other.Value; + } + public override string ToString() => Value; +} \ No newline at end of file diff --git a/Domain/ValueObject/Password.cs b/Domain/ValueObject/Password.cs new file mode 100644 index 0000000..aa40f3e --- /dev/null +++ b/Domain/ValueObject/Password.cs @@ -0,0 +1,37 @@ +using System.Text.RegularExpressions; +using Domain.Entities; + +namespace Domain.ValueObject; + +public class Password +{ + public string Hash { get; private set; } + + private Password() + { + + } + public Password(string hash) + { + if(hash == null) + throw new ArgumentNullException(nameof(hash)); + + Hash = hash; + } + public static Password FromPlainText(string plainText, IHasher hasher) + { + if (!IsValidPlainText(plainText)) + throw new ArgumentException("Invalid password format"); + return new Password(hasher.Hash(plainText)); + } + public static bool IsValidPlainText(string plainText) + { + if (string.IsNullOrWhiteSpace(plainText)) + return false; + + var pattern = @"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$"; + return Regex.IsMatch(plainText, pattern); + } + public bool Verify(string plainText, IHasher hasher) + => hasher.Verify(Hash, plainText); +} \ No newline at end of file diff --git a/Domain/ValueObject/PhoneNumber.cs b/Domain/ValueObject/PhoneNumber.cs new file mode 100644 index 0000000..bdf54e9 --- /dev/null +++ b/Domain/ValueObject/PhoneNumber.cs @@ -0,0 +1,26 @@ +using System.Text.RegularExpressions; + +namespace Domain.ValueObject; + +public class PhoneNumber +{ + public string Value { get; private set; } + private PhoneNumber() + { + } + + public PhoneNumber(string value) + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + if(string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("Phone number cannot be empty or whitespace only string.", nameof(value)); + Value = value; + } + private static bool IsValidPhoneNumber(string number) + { + var pattern = @"^09[0-9]{9}$"; + return Regex.IsMatch(number, pattern); + } +} \ No newline at end of file diff --git a/Infrastructure/DependencyInjection.cs b/Infrastructure/DependencyInjection.cs index bc3d3f9..68a1cc9 100644 --- a/Infrastructure/DependencyInjection.cs +++ b/Infrastructure/DependencyInjection.cs @@ -2,9 +2,8 @@ using Application.Common.Interfaces; using Application.Common.Interfaces.Repositories; using Application.Features.Appointments.Interfaces; -using Application.Features.AppointmentServiceLink.Interfaces; using Application.Features.Auth.Interfaces; -using Application.Features.Service.Interfaces; +using Domain.Repository; using Infrastructure.Persistance; using Infrastructure.Persistance.Repositories; using Infrastructure.Security; @@ -32,7 +31,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); return services; diff --git a/Infrastructure/Persistance/AppDbContext.cs b/Infrastructure/Persistance/AppDbContext.cs index b35ff05..d4f4aff 100644 --- a/Infrastructure/Persistance/AppDbContext.cs +++ b/Infrastructure/Persistance/AppDbContext.cs @@ -1,3 +1,6 @@ +using Domain.Aggregates.Appointment; +using Domain.Aggregates.Service; +using Domain.Aggregates.User; using Domain.Entities; using Infrastructure.Persistance.Configurations; using Microsoft.EntityFrameworkCore; diff --git a/Infrastructure/Persistance/Configurations/AppointmentConfiguration.cs b/Infrastructure/Persistance/Configurations/AppointmentConfiguration.cs index 432b077..c4784bf 100644 --- a/Infrastructure/Persistance/Configurations/AppointmentConfiguration.cs +++ b/Infrastructure/Persistance/Configurations/AppointmentConfiguration.cs @@ -1,3 +1,4 @@ +using Domain.Aggregates.Appointment; using Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/Infrastructure/Persistance/Configurations/ServiceConfiguration.cs b/Infrastructure/Persistance/Configurations/ServiceConfiguration.cs index 631ab74..3ad9542 100644 --- a/Infrastructure/Persistance/Configurations/ServiceConfiguration.cs +++ b/Infrastructure/Persistance/Configurations/ServiceConfiguration.cs @@ -1,3 +1,4 @@ +using Domain.Aggregates.Service; using Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/Infrastructure/Persistance/Configurations/UserConfiguration.cs b/Infrastructure/Persistance/Configurations/UserConfiguration.cs index cda361c..70adbbb 100644 --- a/Infrastructure/Persistance/Configurations/UserConfiguration.cs +++ b/Infrastructure/Persistance/Configurations/UserConfiguration.cs @@ -1,3 +1,4 @@ +using Domain.Aggregates.User; using Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/Infrastructure/Persistance/DbInitializer.cs b/Infrastructure/Persistance/DbInitializer.cs index eb63cd1..0cf98c2 100644 --- a/Infrastructure/Persistance/DbInitializer.cs +++ b/Infrastructure/Persistance/DbInitializer.cs @@ -1,4 +1,5 @@ using Application.Common; +using Domain.Aggregates.User; using Domain.Entities; using Domain.Enums; using Microsoft.Extensions.Configuration; diff --git a/Infrastructure/Persistance/Repositories/AppointmentRepository.cs b/Infrastructure/Persistance/Repositories/AppointmentRepository.cs index dd771f4..cff6b82 100644 --- a/Infrastructure/Persistance/Repositories/AppointmentRepository.cs +++ b/Infrastructure/Persistance/Repositories/AppointmentRepository.cs @@ -1,6 +1,8 @@ using Application.Features.Appointments.DTOs; using Application.Features.Appointments.Interfaces; +using Domain.Aggregates.Appointment; using Domain.Entities; +using Domain.Repository; using Microsoft.EntityFrameworkCore; namespace Infrastructure.Persistance.Repositories; diff --git a/Infrastructure/Persistance/Repositories/AppointmentServiceLinkRepository.cs b/Infrastructure/Persistance/Repositories/AppointmentServiceLinkRepository.cs index 517d4ed..4043c1a 100644 --- a/Infrastructure/Persistance/Repositories/AppointmentServiceLinkRepository.cs +++ b/Infrastructure/Persistance/Repositories/AppointmentServiceLinkRepository.cs @@ -1,5 +1,5 @@ -using Application.Features.AppointmentServiceLink.Interfaces; using Domain.Entities; +using Domain.Repository; using Microsoft.EntityFrameworkCore; namespace Infrastructure.Persistance.Repositories; diff --git a/Infrastructure/Persistance/Repositories/ServiceRepository.cs b/Infrastructure/Persistance/Repositories/ServiceRepository.cs index 36191b9..0a38b59 100644 --- a/Infrastructure/Persistance/Repositories/ServiceRepository.cs +++ b/Infrastructure/Persistance/Repositories/ServiceRepository.cs @@ -1,6 +1,7 @@ using Application.Features.Service.DTOs; -using Application.Features.Service.Interfaces; +using Domain.Aggregates.Service; using Domain.Entities; +using Domain.Repository; using Microsoft.EntityFrameworkCore; namespace Infrastructure.Persistance.Repositories; diff --git a/Infrastructure/Persistance/Repositories/UserRepository.cs b/Infrastructure/Persistance/Repositories/UserRepository.cs index faa8485..cd63a1e 100644 --- a/Infrastructure/Persistance/Repositories/UserRepository.cs +++ b/Infrastructure/Persistance/Repositories/UserRepository.cs @@ -1,6 +1,8 @@ using Application.Features.Auth.DTOs; using Application.Features.Auth.Interfaces; +using Domain.Aggregates.User; using Domain.Entities; +using Domain.Repository; using Microsoft.EntityFrameworkCore; namespace Infrastructure.Persistance.Repositories; diff --git a/Infrastructure/Security/Context/UserContext.cs b/Infrastructure/Security/Context/UserContext.cs index 3255df5..e05ea79 100644 --- a/Infrastructure/Security/Context/UserContext.cs +++ b/Infrastructure/Security/Context/UserContext.cs @@ -5,7 +5,7 @@ namespace Infrastructure.Security.Context; -public class UserContext : IUSerContext +public class UserContext : IUserContext { private readonly IHttpContextAccessor _httpContextAccessor; diff --git a/Infrastructure/Security/Jwt/JwtTokenService.cs b/Infrastructure/Security/Jwt/JwtTokenService.cs index 5aca60d..6368aaf 100644 --- a/Infrastructure/Security/Jwt/JwtTokenService.cs +++ b/Infrastructure/Security/Jwt/JwtTokenService.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography; using System.Text; using Application.Common.Interfaces; +using Domain.Aggregates.User; using Domain.Entities; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; @@ -32,7 +33,7 @@ public string GenerateJwtToken(User user) Encoding.UTF8.GetBytes(_configuration["JwtSettings:SecretKey"]) ); - var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var expiresMinutes = int.Parse(_configuration["JwtSettings:AccessTokenMinutes"]); var token = new JwtSecurityToken( diff --git a/api/Controllers/UserController.cs b/api/Controllers/UserController.cs index 02711cb..eeab311 100644 --- a/api/Controllers/UserController.cs +++ b/api/Controllers/UserController.cs @@ -60,4 +60,5 @@ public async Task UpdateProfile([FromBody] UpdateProfileRequ { return await _service.UpdateProfileAsync(updateProfileRequestDto); } + } \ No newline at end of file diff --git a/api/Program.cs b/api/Program.cs index a9c0357..5ec38c8 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -53,14 +53,13 @@ { options.RequireHttpsMetadata = false; options.SaveToken = true; - options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateIssuerSigningKey = true, ValidateLifetime = true, - + ValidIssuer = builder.Configuration["JwtSettings:Issuer"], ValidAudience = builder.Configuration["JwtSettings:Audience"],