From 00aa3ab6af81d7b87c81bfc90ec370dcb44adc67 Mon Sep 17 00:00:00 2001 From: prtsie Date: Sun, 25 Aug 2024 19:49:28 +0300 Subject: [PATCH] Added validation and fixed errors --- .../ApplicationLayer/ApplicationLayer.csproj | 2 + .../ApplicationLayer/DependencyInjection.cs | 6 +- .../IUserIdProvider.cs | 8 ++ .../Applicants/Models/PlaceOfWorkModel.cs | 16 ++++ .../NeededServices/IApplicantsRepository.cs | 3 + .../Services/AuthServices/Common/AuthData.cs | 4 + .../LoginService/DevelopmentLoginService.cs | 9 +- .../LoginService/ILoginService.cs | 10 +-- .../AuthServices/LoginService/LoginService.cs | 7 +- .../Exceptions/UserAlreadyExistsException.cs | 2 +- .../RegisterService/RegisterService.cs | 37 +-------- .../Requests/RegisterApplicantRequest.cs | 11 +-- .../AuthServices/Requests/RegisterRequest.cs | 6 +- .../AuthServices/Requests/UserLoginRequest.cs | 4 - .../Requests/Validation/AuthDataValidator.cs | 32 ++++++++ .../Requests/Validation/NameValidator.cs | 28 +++++++ .../Requests/Validation/PassportValidator.cs | 37 +++++++++ .../Validation/PlaceOfWorkModelValidator.cs | 50 +++++++++++ .../RegisterApplicantRequestValidator.cs | 78 ++++++++++++++++++ .../IUsersService.cs | 9 +- .../Requests/ChangeUserAuthDataRequest.cs | 6 ++ .../UsersService.cs | 12 +-- .../Requests/Validation/PastVisaValidator.cs | 28 +++++++ .../Requests/Validation/PastVisitValidator.cs | 31 +++++++ .../PermissionToDestCountryValidator.cs | 25 ++++++ .../Validation/ReentryPermitValidator.cs | 25 ++++++ .../VisaApplicationCreateRequestValidator.cs | 63 ++++++++++++++ .../Requests/VisaApplicationCreateRequest.cs | 2 +- .../ConfigurationConstraints.cs | 8 +- .../Automapper/Profiles/ApplicantProfile.cs | 6 ++ .../Automapper/Profiles/PlaceOfWorkProfile.cs | 16 ++++ .../Automapper/Profiles/UserProfile.cs | 8 +- .../Infrastructure/Common/UserIdProvider.cs | 19 +++++ .../Configuration/ApplicantConfiguration.cs | 7 +- .../Configuration/PlaceOfWorkConfiguration.cs | 3 +- .../Repositories/ApplicantsRepository.cs | 9 +- .../Users/Configuration/UserConfiguration.cs | 3 +- .../VisaApplicationConfiguration.cs | 3 +- .../Infrastructure/DependencyInjection.cs | 3 + .../Controllers/UsersController.cs | 27 ++++-- .../Controllers/VisaApiControllerBase.cs | 12 --- .../Controllers/VisaApplicationController.cs | 11 ++- .../GlobalExceptionsFilter.cs | 82 +++++++++++-------- 43 files changed, 621 insertions(+), 147 deletions(-) create mode 100644 SchengenVisaApi/ApplicationLayer/InfrastructureServicesInterfaces/IUserIdProvider.cs create mode 100644 SchengenVisaApi/ApplicationLayer/Services/Applicants/Models/PlaceOfWorkModel.cs create mode 100644 SchengenVisaApi/ApplicationLayer/Services/AuthServices/Common/AuthData.cs delete mode 100644 SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/UserLoginRequest.cs create mode 100644 SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/AuthDataValidator.cs create mode 100644 SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/NameValidator.cs create mode 100644 SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/PassportValidator.cs create mode 100644 SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/PlaceOfWorkModelValidator.cs create mode 100644 SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/RegisterApplicantRequestValidator.cs rename SchengenVisaApi/ApplicationLayer/Services/{ApprovingAuthorities => Users}/IUsersService.cs (65%) create mode 100644 SchengenVisaApi/ApplicationLayer/Services/Users/Requests/ChangeUserAuthDataRequest.cs rename SchengenVisaApi/ApplicationLayer/Services/{ApprovingAuthorities => Users}/UsersService.cs (68%) create mode 100644 SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/PastVisaValidator.cs create mode 100644 SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/PastVisitValidator.cs create mode 100644 SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/PermissionToDestCountryValidator.cs create mode 100644 SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/ReentryPermitValidator.cs create mode 100644 SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/VisaApplicationCreateRequestValidator.cs rename SchengenVisaApi/{Infrastructure/Database => Domains}/ConfigurationConstraints.cs (72%) create mode 100644 SchengenVisaApi/Infrastructure/Automapper/Profiles/PlaceOfWorkProfile.cs create mode 100644 SchengenVisaApi/Infrastructure/Common/UserIdProvider.cs delete mode 100644 SchengenVisaApi/SchengenVisaApi/Controllers/VisaApiControllerBase.cs diff --git a/SchengenVisaApi/ApplicationLayer/ApplicationLayer.csproj b/SchengenVisaApi/ApplicationLayer/ApplicationLayer.csproj index bc6298d..dd0002f 100644 --- a/SchengenVisaApi/ApplicationLayer/ApplicationLayer.csproj +++ b/SchengenVisaApi/ApplicationLayer/ApplicationLayer.csproj @@ -11,6 +11,8 @@ + + diff --git a/SchengenVisaApi/ApplicationLayer/DependencyInjection.cs b/SchengenVisaApi/ApplicationLayer/DependencyInjection.cs index 14e9ffe..0d9a2eb 100644 --- a/SchengenVisaApi/ApplicationLayer/DependencyInjection.cs +++ b/SchengenVisaApi/ApplicationLayer/DependencyInjection.cs @@ -1,7 +1,9 @@ -using ApplicationLayer.Services.ApprovingAuthorities; +using System.Reflection; using ApplicationLayer.Services.AuthServices.LoginService; using ApplicationLayer.Services.AuthServices.RegisterService; +using ApplicationLayer.Services.Users; using ApplicationLayer.Services.VisaApplications.Handlers; +using FluentValidation; using Microsoft.Extensions.DependencyInjection; namespace ApplicationLayer; @@ -12,6 +14,8 @@ public static class DependencyInjection /// Add services for Application layer public static IServiceCollection AddApplicationLayer(this IServiceCollection services, bool isDevelopment = false) { + services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); + services.AddScoped(); services.AddScoped(); diff --git a/SchengenVisaApi/ApplicationLayer/InfrastructureServicesInterfaces/IUserIdProvider.cs b/SchengenVisaApi/ApplicationLayer/InfrastructureServicesInterfaces/IUserIdProvider.cs new file mode 100644 index 0000000..b7e1101 --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/InfrastructureServicesInterfaces/IUserIdProvider.cs @@ -0,0 +1,8 @@ +namespace ApplicationLayer.InfrastructureServicesInterfaces +{ + public interface IUserIdProvider + { + /// Returns identifier of authenticated user who sent the request + Guid GetUserId(); + } +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/Applicants/Models/PlaceOfWorkModel.cs b/SchengenVisaApi/ApplicationLayer/Services/Applicants/Models/PlaceOfWorkModel.cs new file mode 100644 index 0000000..35ae398 --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/Applicants/Models/PlaceOfWorkModel.cs @@ -0,0 +1,16 @@ +using Domains.ApplicantDomain; + +namespace ApplicationLayer.Services.Applicants.Models +{ + public class PlaceOfWorkModel + { + /// Name of hirer + public string Name { get; set; } = null!; + + /// Address of hirer + public Address Address { get; set; } = null!; + + /// Phone number of hirer + public string PhoneNum { get; set; } = null!; + } +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/Applicants/NeededServices/IApplicantsRepository.cs b/SchengenVisaApi/ApplicationLayer/Services/Applicants/NeededServices/IApplicantsRepository.cs index 065734b..fa2ee5f 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/Applicants/NeededServices/IApplicantsRepository.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/Applicants/NeededServices/IApplicantsRepository.cs @@ -11,4 +11,7 @@ public interface IApplicantsRepository : IGenericRepository /// Get identifier of applicant by user identifier Task GetApplicantIdByUserId(Guid userId, CancellationToken cancellationToken); + + /// Returns value of NonResident property of applicant + Task IsApplicantNonResidentByUserId(Guid userId, CancellationToken cancellationToken); } diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Common/AuthData.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Common/AuthData.cs new file mode 100644 index 0000000..a6cec9c --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Common/AuthData.cs @@ -0,0 +1,4 @@ +namespace ApplicationLayer.Services.AuthServices.Common +{ + public record AuthData(string Email, string Password); +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/LoginService/DevelopmentLoginService.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/LoginService/DevelopmentLoginService.cs index d7678ce..2e9d245 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/LoginService/DevelopmentLoginService.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/LoginService/DevelopmentLoginService.cs @@ -1,23 +1,22 @@ using ApplicationLayer.Services.AuthServices.LoginService.Exceptions; using ApplicationLayer.Services.AuthServices.NeededServices; -using ApplicationLayer.Services.AuthServices.Requests; using Domains.Users; namespace ApplicationLayer.Services.AuthServices.LoginService { public class DevelopmentLoginService(IUsersRepository users, ITokenGenerator tokenGenerator) : ILoginService { - async Task ILoginService.LoginAsync(UserLoginRequest request, CancellationToken cancellationToken) + async Task ILoginService.LoginAsync(string email, string password, CancellationToken cancellationToken) { - if (request is { Email: "admin@mail.ru", Password: "admin" }) + if (email == "admin@mail.ru" && password == "admin") { var admin = new User { Role = Role.Admin }; return tokenGenerator.CreateToken(admin); } - var user = await users.FindByEmailAsync(request.Email, cancellationToken); - if (user is null || user.Password != request.Password) + var user = await users.FindByEmailAsync(email, cancellationToken); + if (user is null || user.Password != password) { throw new IncorrectLoginDataException(); } diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/LoginService/ILoginService.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/LoginService/ILoginService.cs index 4d97c4c..71599c9 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/LoginService/ILoginService.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/LoginService/ILoginService.cs @@ -1,12 +1,10 @@ -using ApplicationLayer.Services.AuthServices.Requests; - -namespace ApplicationLayer.Services.AuthServices.LoginService +namespace ApplicationLayer.Services.AuthServices.LoginService { - /// Handles + /// Handles login requests public interface ILoginService { - /// Handle + /// Handle login request /// JWT-token - Task LoginAsync(UserLoginRequest request, CancellationToken cancellationToken); + Task LoginAsync(string email, string password, CancellationToken cancellationToken); } } diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/LoginService/LoginService.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/LoginService/LoginService.cs index 18e844a..8697956 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/LoginService/LoginService.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/LoginService/LoginService.cs @@ -1,16 +1,15 @@ using ApplicationLayer.Services.AuthServices.LoginService.Exceptions; using ApplicationLayer.Services.AuthServices.NeededServices; -using ApplicationLayer.Services.AuthServices.Requests; namespace ApplicationLayer.Services.AuthServices.LoginService { /// public class LoginService(IUsersRepository users, ITokenGenerator tokenGenerator) : ILoginService { - async Task ILoginService.LoginAsync(UserLoginRequest request, CancellationToken cancellationToken) + async Task ILoginService.LoginAsync(string email, string password, CancellationToken cancellationToken) { - var user = await users.FindByEmailAsync(request.Email, cancellationToken); - if (user is null || user.Password != request.Password) + var user = await users.FindByEmailAsync(email, cancellationToken); + if (user is null || user.Password != password) { throw new IncorrectLoginDataException(); } diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/Exceptions/UserAlreadyExistsException.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/Exceptions/UserAlreadyExistsException.cs index 98cf71a..b67668b 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/Exceptions/UserAlreadyExistsException.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/Exceptions/UserAlreadyExistsException.cs @@ -3,5 +3,5 @@ using ApplicationLayer.Services.AuthServices.Requests; namespace ApplicationLayer.Services.AuthServices.RegisterService.Exceptions { - public class UserAlreadyExistsException(RegisterRequest request) : AlreadyExistsException($"User with email '{request.Email}' already exists"); + public class UserAlreadyExistsException(RegisterRequest request) : AlreadyExistsException($"User with email '{request.AuthData.Email}' already exists"); } diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/RegisterService.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/RegisterService.cs index 6315d9d..86be1f9 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/RegisterService.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/RegisterService.cs @@ -1,7 +1,6 @@ using ApplicationLayer.InfrastructureServicesInterfaces; using ApplicationLayer.Services.Applicants.NeededServices; using ApplicationLayer.Services.AuthServices.NeededServices; -using ApplicationLayer.Services.AuthServices.RegisterService.Exceptions; using ApplicationLayer.Services.AuthServices.Requests; using AutoMapper; using Domains.ApplicantDomain; @@ -18,33 +17,11 @@ namespace ApplicationLayer.Services.AuthServices.RegisterService { async Task IRegisterService.RegisterApplicant(RegisterApplicantRequest request, CancellationToken cancellationToken) { - //todo move to validation layer - if (await users.FindByEmailAsync(request.Email, cancellationToken) is not null) - { - throw new UserAlreadyExistsException(request); - } - - var user = mapper.Map(request); + var user = mapper.Map(request.AuthData); user.Role = Role.Applicant; - var applicant = new Applicant - { - Citizenship = request.Citizenship, - CitizenshipByBirth = request.CitizenshipByBirth, - Gender = request.Gender, - Name = request.ApplicantName, - Passport = request.Passport, - BirthDate = request.BirthDate, - FatherName = request.FatherName, - JobTitle = request.JobTitle, - MaritalStatus = request.MaritalStatus, - MotherName = request.MotherName, - UserId = user.Id, - CityOfBirth = request.CityOfBirth, - CountryOfBirth = request.CountryOfBirth, - IsNonResident = request.IsNonResident, - PlaceOfWork = request.PlaceOfWork - }; + var applicant = mapper.Map(request); + applicant.UserId = user.Id; await users.AddAsync(user, cancellationToken); await applicants.AddAsync(applicant, cancellationToken); @@ -54,13 +31,7 @@ namespace ApplicationLayer.Services.AuthServices.RegisterService async Task IRegisterService.RegisterAuthority(RegisterRequest request, CancellationToken cancellationToken) { - //todo move to validation layer - if (await users.FindByEmailAsync(request.Email, cancellationToken) is not null) - { - throw new UserAlreadyExistsException(request); - } - - var user = mapper.Map(request); + var user = mapper.Map(request.AuthData); user.Role = Role.ApprovingAuthority; await users.AddAsync(user, cancellationToken); diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/RegisterApplicantRequest.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/RegisterApplicantRequest.cs index e9b5d9a..57171f5 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/RegisterApplicantRequest.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/RegisterApplicantRequest.cs @@ -1,10 +1,11 @@ -using Domains.ApplicantDomain; +using ApplicationLayer.Services.Applicants.Models; +using ApplicationLayer.Services.AuthServices.Common; +using Domains.ApplicantDomain; namespace ApplicationLayer.Services.AuthServices.Requests { public record RegisterApplicantRequest( - string Email, - string Password, + AuthData AuthData, Name ApplicantName, Passport Passport, DateTime BirthDate, @@ -17,6 +18,6 @@ namespace ApplicationLayer.Services.AuthServices.Requests Name FatherName, Name MotherName, string JobTitle, - PlaceOfWork PlaceOfWork, - bool IsNonResident) : RegisterRequest(Email, Password); + PlaceOfWorkModel PlaceOfWork, + bool IsNonResident) : RegisterRequest(AuthData); } diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/RegisterRequest.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/RegisterRequest.cs index 990dbf6..82422ce 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/RegisterRequest.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/RegisterRequest.cs @@ -1,4 +1,6 @@ -namespace ApplicationLayer.Services.AuthServices.Requests +using ApplicationLayer.Services.AuthServices.Common; + +namespace ApplicationLayer.Services.AuthServices.Requests { - public record RegisterRequest(string Email, string Password); + public record RegisterRequest(AuthData AuthData); } diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/UserLoginRequest.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/UserLoginRequest.cs deleted file mode 100644 index 1249392..0000000 --- a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/UserLoginRequest.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace ApplicationLayer.Services.AuthServices.Requests -{ - public record UserLoginRequest(string Email, string Password); -} diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/AuthDataValidator.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/AuthDataValidator.cs new file mode 100644 index 0000000..d988e21 --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/AuthDataValidator.cs @@ -0,0 +1,32 @@ +using ApplicationLayer.Services.AuthServices.Common; +using ApplicationLayer.Services.AuthServices.NeededServices; +using Domains; +using FluentValidation; + +namespace ApplicationLayer.Services.AuthServices.Requests.Validation +{ + public class AuthDataValidator : AbstractValidator + { + public AuthDataValidator(IUsersRepository users) + { + RuleFor(d => d.Email) + .NotEmpty() + .WithMessage("Email can not be empty") + .EmailAddress() + .WithMessage("Email must be valid") + .MaximumLength(ConfigurationConstraints.EmailLength) + .WithMessage($"Email length must be less than {ConfigurationConstraints.EmailLength}") + .MustAsync(async (email, ct) => + { + return await users.FindByEmailAsync(email, ct) is null; + }) + .WithMessage("Email already exists"); + + RuleFor(d => d.Password) + .NotEmpty() + .WithMessage("Password can not be empty") + .MaximumLength(ConfigurationConstraints.PasswordLength) + .WithMessage($"Password length must be less than {ConfigurationConstraints.PasswordLength}"); + } + } +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/NameValidator.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/NameValidator.cs new file mode 100644 index 0000000..38547df --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/NameValidator.cs @@ -0,0 +1,28 @@ +using Domains; +using Domains.ApplicantDomain; +using FluentValidation; + +namespace ApplicationLayer.Services.AuthServices.Requests.Validation +{ + public class NameValidator : AbstractValidator + { + public NameValidator() + { + RuleFor(m => m.FirstName) + .NotEmpty() + .WithMessage("First Name can not be empty") + .MaximumLength(ConfigurationConstraints.NameLength) + .WithMessage($"First Name length must be less than {ConfigurationConstraints.NameLength}"); + + RuleFor(m => m.Surname) + .NotEmpty() + .WithMessage("Surname can not be empty") + .MaximumLength(ConfigurationConstraints.NameLength) + .WithMessage($"Surname length must be less than {ConfigurationConstraints.NameLength}"); + + RuleFor(m => m.Patronymic) + .MaximumLength(ConfigurationConstraints.NameLength) + .WithMessage($"Patronymic length must be less than {ConfigurationConstraints.NameLength}"); + } + } +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/PassportValidator.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/PassportValidator.cs new file mode 100644 index 0000000..60b96c1 --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/PassportValidator.cs @@ -0,0 +1,37 @@ +using ApplicationLayer.InfrastructureServicesInterfaces; +using Domains; +using Domains.ApplicantDomain; +using FluentValidation; + +namespace ApplicationLayer.Services.AuthServices.Requests.Validation +{ + public class PassportValidator : AbstractValidator + { + public PassportValidator(IDateTimeProvider dateTimeProvider) + { + RuleFor(r => r.Issuer) + .NotEmpty() + .WithMessage("Passport issuer can not be empty") + .MaximumLength(ConfigurationConstraints.IssuerNameLength) + .WithMessage($"Passport issuer length must be less than {ConfigurationConstraints.IssuerNameLength}"); + + RuleFor(r => r.Number) + .NotEmpty() + .WithMessage("Passport number can not be empty") + .MaximumLength(ConfigurationConstraints.PassportNumberLength) + .WithMessage($"Passport number length must be less than {ConfigurationConstraints.PassportNumberLength}"); + + RuleFor(r => r.ExpirationDate) + .NotEmpty() + .WithMessage("Passport expiration date can not be empty") + .GreaterThan(dateTimeProvider.Now()) + .WithMessage("Can not approve visa for applicants with expired passport"); + + RuleFor(r => r.IssueDate) + .NotEmpty() + .WithMessage("Passport issue date can not be empty") + .LessThanOrEqualTo(dateTimeProvider.Now()) + .WithMessage("Passport issue date must be in past"); + } + } +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/PlaceOfWorkModelValidator.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/PlaceOfWorkModelValidator.cs new file mode 100644 index 0000000..ec01684 --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/PlaceOfWorkModelValidator.cs @@ -0,0 +1,50 @@ +using ApplicationLayer.Services.Applicants.Models; +using Domains; +using FluentValidation; + +namespace ApplicationLayer.Services.AuthServices.Requests.Validation +{ + public class PlaceOfWorkModelValidator : AbstractValidator + { + public PlaceOfWorkModelValidator() + { + RuleFor(p => p.Name) + .NotEmpty() + .WithMessage("Place of work name can not be empty") + .MaximumLength(ConfigurationConstraints.PlaceOfWorkNameLength) + .WithMessage($"Place of work name length must be less than {ConfigurationConstraints.PlaceOfWorkNameLength}"); + + RuleFor(p => p.PhoneNum) + .NotEmpty() + .WithMessage("Place of work phone number can not be empty") + .MaximumLength(ConfigurationConstraints.PhoneNumberLength) + .WithMessage($"Phone number length must be in range from {ConfigurationConstraints.PhoneNumberMinLength} to {ConfigurationConstraints.PhoneNumberLength}") + .MinimumLength(ConfigurationConstraints.PhoneNumberMinLength) + .WithMessage($"Phone number length must be in range from {ConfigurationConstraints.PhoneNumberMinLength} to {ConfigurationConstraints.PhoneNumberLength}"); + + RuleFor(p => p.Address.Country) + .NotEmpty() + .WithMessage("Country name of place of work can not be empty") + .MaximumLength(ConfigurationConstraints.CountryNameLength) + .WithMessage($"Country name of place of work length must be less than {ConfigurationConstraints.CountryNameLength}"); + + RuleFor(p => p.Address.City) + .NotEmpty() + .WithMessage("City name of place of work can not be empty") + .MaximumLength(ConfigurationConstraints.CityNameLength) + .WithMessage($"City name of place of work length must be less than {ConfigurationConstraints.CityNameLength}"); + + RuleFor(p => p.Address.Street) + .NotEmpty() + .WithMessage("Street name of place of work can not be empty") + .MaximumLength(ConfigurationConstraints.StreetNameLength) + .WithMessage($"Street name of place of work length must be less than {ConfigurationConstraints.StreetNameLength}"); + + RuleFor(p => p.Address.Building) + .NotEmpty() + .WithMessage("Building of place of work can not be empty") + .MaximumLength(ConfigurationConstraints.CountryNameLength) + .WithMessage($"Building of place of work length must be less than {ConfigurationConstraints.BuildingNumberLength}"); + } + } +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/RegisterApplicantRequestValidator.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/RegisterApplicantRequestValidator.cs new file mode 100644 index 0000000..da869d9 --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/Requests/Validation/RegisterApplicantRequestValidator.cs @@ -0,0 +1,78 @@ +using ApplicationLayer.InfrastructureServicesInterfaces; +using ApplicationLayer.Services.Applicants.Models; +using ApplicationLayer.Services.AuthServices.Common; +using Domains; +using Domains.ApplicantDomain; +using FluentValidation; + +namespace ApplicationLayer.Services.AuthServices.Requests.Validation +{ + public class RegisterApplicantRequestValidator : AbstractValidator + { + public RegisterApplicantRequestValidator( + IDateTimeProvider dateTimeProvider, + IValidator nameValidator, + IValidator authDataValidator, + IValidator passportValidator, + IValidator placeOfWorkModelValidator) + { + RuleFor(r => r.AuthData) + .SetValidator(authDataValidator); + + RuleFor(r => r.ApplicantName) + .SetValidator(nameValidator); + + RuleFor(r => r.FatherName) + .SetValidator(nameValidator); + + RuleFor(r => r.MotherName) + .SetValidator(nameValidator); + + RuleFor(r => r.Passport) + .SetValidator(passportValidator); + + RuleFor(r => r.BirthDate) + .NotEmpty() + .WithMessage("Birth date can not be empty") + .LessThanOrEqualTo(dateTimeProvider.Now().AddYears(-ConfigurationConstraints.ApplicantMinAge)) + .WithMessage($"Applicant must be older than {ConfigurationConstraints.ApplicantMinAge}"); + + RuleFor(r => r.CountryOfBirth) + .NotEmpty() + .WithMessage("Country of birth can not be empty") + .MaximumLength(ConfigurationConstraints.CountryNameLength) + .WithMessage($"Country of birth name length must be less than {ConfigurationConstraints.CountryNameLength}"); + + RuleFor(r => r.CityOfBirth) + .NotEmpty() + .WithMessage("City of birth can not be empty") + .MaximumLength(ConfigurationConstraints.CityNameLength) + .WithMessage($"City of birth name length must be less than {ConfigurationConstraints.CityNameLength}"); + + RuleFor(r => r.Citizenship) + .NotEmpty() + .WithMessage("Citizenship can not be empty") + .MaximumLength(ConfigurationConstraints.CitizenshipLength) + .WithMessage($"Citizenship length must be less than {ConfigurationConstraints.CitizenshipLength}"); + + RuleFor(r => r.CitizenshipByBirth) + .NotEmpty() + .WithMessage("Citizenship by birth can not be empty") + .MaximumLength(ConfigurationConstraints.CitizenshipLength) + .WithMessage($"Citizenship by birth length must be less than {ConfigurationConstraints.CitizenshipLength}"); + + RuleFor(r => r.Gender).IsInEnum(); + + RuleFor(r => r.MaritalStatus).IsInEnum(); + + RuleFor(r => r.JobTitle) + .NotEmpty() + .WithMessage("Title of job can not be empty") + .MaximumLength(ConfigurationConstraints.JobTitleLength) + .WithMessage($"Title of job length must be less than {ConfigurationConstraints.JobTitleLength}"); + + RuleFor(r => r.PlaceOfWork) + .SetValidator(placeOfWorkModelValidator); + } + } +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/ApprovingAuthorities/IUsersService.cs b/SchengenVisaApi/ApplicationLayer/Services/Users/IUsersService.cs similarity index 65% rename from SchengenVisaApi/ApplicationLayer/Services/ApprovingAuthorities/IUsersService.cs rename to SchengenVisaApi/ApplicationLayer/Services/Users/IUsersService.cs index 500f9c2..95a4648 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/ApprovingAuthorities/IUsersService.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/Users/IUsersService.cs @@ -1,7 +1,7 @@ -using ApplicationLayer.Services.AuthServices.Requests; +using ApplicationLayer.Services.Users.Requests; using Domains.Users; -namespace ApplicationLayer.Services.ApprovingAuthorities +namespace ApplicationLayer.Services.Users { /// user accounts service public interface IUsersService @@ -11,10 +11,9 @@ namespace ApplicationLayer.Services.ApprovingAuthorities Task> GetAuthoritiesAccountsAsync(CancellationToken cancellationToken); /// Changes authentication data for an account - /// identifier of account - /// request data with new email and password + /// Request object with identifier of user and new authentication data /// Cancellation token - Task ChangeAccountAuthDataAsync(Guid userId, RegisterRequest data, CancellationToken cancellationToken); + Task ChangeAccountAuthDataAsync(ChangeUserAuthDataRequest request, CancellationToken cancellationToken); /// Removes user account /// Identifier of account diff --git a/SchengenVisaApi/ApplicationLayer/Services/Users/Requests/ChangeUserAuthDataRequest.cs b/SchengenVisaApi/ApplicationLayer/Services/Users/Requests/ChangeUserAuthDataRequest.cs new file mode 100644 index 0000000..06b10e7 --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/Users/Requests/ChangeUserAuthDataRequest.cs @@ -0,0 +1,6 @@ +using ApplicationLayer.Services.AuthServices.Common; + +namespace ApplicationLayer.Services.Users.Requests +{ + public record ChangeUserAuthDataRequest(Guid UserId, AuthData NewAuthData); +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/ApprovingAuthorities/UsersService.cs b/SchengenVisaApi/ApplicationLayer/Services/Users/UsersService.cs similarity index 68% rename from SchengenVisaApi/ApplicationLayer/Services/ApprovingAuthorities/UsersService.cs rename to SchengenVisaApi/ApplicationLayer/Services/Users/UsersService.cs index ab53755..d088bac 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/ApprovingAuthorities/UsersService.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/Users/UsersService.cs @@ -1,9 +1,9 @@ using ApplicationLayer.InfrastructureServicesInterfaces; using ApplicationLayer.Services.AuthServices.NeededServices; -using ApplicationLayer.Services.AuthServices.Requests; +using ApplicationLayer.Services.Users.Requests; using Domains.Users; -namespace ApplicationLayer.Services.ApprovingAuthorities +namespace ApplicationLayer.Services.Users { public class UsersService(IUsersRepository users, IUnitOfWork unitOfWork) : IUsersService { @@ -12,12 +12,12 @@ namespace ApplicationLayer.Services.ApprovingAuthorities return await users.GetAllOfRoleAsync(Role.ApprovingAuthority, cancellationToken); } - async Task IUsersService.ChangeAccountAuthDataAsync(Guid userId, RegisterRequest data, CancellationToken cancellationToken) + async Task IUsersService.ChangeAccountAuthDataAsync(ChangeUserAuthDataRequest request, CancellationToken cancellationToken) { - var user = await users.GetByIdAsync(userId, cancellationToken); + var user = await users.GetByIdAsync(request.UserId, cancellationToken); - user.Email = data.Email; - user.Password = data.Password; + user.Email = request.NewAuthData.Email; + user.Password = request.NewAuthData.Password; await users.UpdateAsync(user, cancellationToken); await unitOfWork.SaveAsync(cancellationToken); diff --git a/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/PastVisaValidator.cs b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/PastVisaValidator.cs new file mode 100644 index 0000000..c8fb60a --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/PastVisaValidator.cs @@ -0,0 +1,28 @@ +using ApplicationLayer.InfrastructureServicesInterfaces; +using Domains.VisaApplicationDomain; +using FluentValidation; + +namespace ApplicationLayer.Services.VisaApplications.Requests.Validation +{ + public class PastVisaValidator : AbstractValidator + { + public PastVisaValidator(IDateTimeProvider dateTimeProvider) + { + RuleFor(v => v.ExpirationDate) + .NotEmpty() + .WithMessage("Expiration date of past visa can not be empty") + .GreaterThan(v => v.IssueDate) + .WithMessage("Past visa expiration date can not be earlier than issue date"); + + RuleFor(v => v.IssueDate) + .NotEmpty() + .WithMessage("Issue date of past visa can not be empty") + .LessThan(dateTimeProvider.Now()) + .WithMessage("Issue date of past visa must be in past"); + + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage("Name of past visa can not be empty"); + } + } +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/PastVisitValidator.cs b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/PastVisitValidator.cs new file mode 100644 index 0000000..9e46a30 --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/PastVisitValidator.cs @@ -0,0 +1,31 @@ +using ApplicationLayer.InfrastructureServicesInterfaces; +using Domains; +using Domains.VisaApplicationDomain; +using FluentValidation; + +namespace ApplicationLayer.Services.VisaApplications.Requests.Validation +{ + public class PastVisitValidator : AbstractValidator + { + public PastVisitValidator(IDateTimeProvider dateTimeProvider) + { + RuleFor(v => v.StartDate) + .NotEmpty() + .WithMessage("Start date of past visit can not be empty") + .LessThan(v => v.EndDate) + .WithMessage("Start date of past visit must be earlier than end date") + .LessThan(dateTimeProvider.Now()) + .WithMessage("Start date of past visit must be in past"); + + RuleFor(v => v.EndDate) + .NotEmpty() + .WithMessage("End date of past visit can not be empty"); + + RuleFor(v => v.DestinationCountry) + .NotEmpty() + .WithMessage("Destination Country of past visit can not be null") + .MaximumLength(ConfigurationConstraints.CountryNameLength) + .WithMessage($"Destination Country of past visit length must be less than {ConfigurationConstraints.CountryNameLength}"); + } + } +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/PermissionToDestCountryValidator.cs b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/PermissionToDestCountryValidator.cs new file mode 100644 index 0000000..1116730 --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/PermissionToDestCountryValidator.cs @@ -0,0 +1,25 @@ +using ApplicationLayer.InfrastructureServicesInterfaces; +using Domains; +using Domains.VisaApplicationDomain; +using FluentValidation; + +namespace ApplicationLayer.Services.VisaApplications.Requests.Validation +{ + public class PermissionToDestCountryValidator : AbstractValidator + { + public PermissionToDestCountryValidator(IDateTimeProvider dateTimeProvider) + { + RuleFor(p => p!.ExpirationDate) + .NotEmpty() + .WithMessage("Expiration date of permission to destination Country can not be empty") + .GreaterThan(dateTimeProvider.Now()) + .WithMessage("Permission to destination Country must not be expired"); + + RuleFor(p => p!.Issuer) + .NotEmpty() + .WithMessage("Issuer of permission for destination Country can not be empty") + .MaximumLength(ConfigurationConstraints.IssuerNameLength) + .WithMessage($"Issuer of permission to destination Country length must be less than {ConfigurationConstraints.IssuerNameLength}"); + } + } +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/ReentryPermitValidator.cs b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/ReentryPermitValidator.cs new file mode 100644 index 0000000..ff59c98 --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/ReentryPermitValidator.cs @@ -0,0 +1,25 @@ +using ApplicationLayer.InfrastructureServicesInterfaces; +using Domains; +using Domains.VisaApplicationDomain; +using FluentValidation; + +namespace ApplicationLayer.Services.VisaApplications.Requests.Validation +{ + public class ReentryPermitValidator : AbstractValidator + { + public ReentryPermitValidator(IDateTimeProvider dateTimeProvider) + { + RuleFor(p => p!.Number) + .NotEmpty() + .WithMessage("Re-entry permit number can not be empty") + .MaximumLength(ConfigurationConstraints.ReentryPermitNumberLength) + .WithMessage($"Re-entry permit number length must be less than {ConfigurationConstraints.ReentryPermitNumberLength}"); + + RuleFor(p => p!.ExpirationDate) + .NotEmpty() + .WithMessage("Re-entry permit expiration date can not be empty") + .GreaterThan(dateTimeProvider.Now()) + .WithMessage("Re-entry permit must not be expired"); + } + } +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/VisaApplicationCreateRequestValidator.cs b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/VisaApplicationCreateRequestValidator.cs new file mode 100644 index 0000000..0d12f7d --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/VisaApplicationCreateRequestValidator.cs @@ -0,0 +1,63 @@ +using ApplicationLayer.InfrastructureServicesInterfaces; +using ApplicationLayer.Services.Applicants.NeededServices; +using Domains; +using Domains.VisaApplicationDomain; +using FluentValidation; + +namespace ApplicationLayer.Services.VisaApplications.Requests.Validation +{ + public class VisaApplicationCreateRequestValidator : AbstractValidator + { + public VisaApplicationCreateRequestValidator( + IValidator reentryPermitValidator, + IValidator pastVisaValidator, + IValidator permissionToDestCountryValidator, + IValidator pastVisitValidator, + IApplicantsRepository applicants, + IUserIdProvider userIdProvider) + { + //todo fix + WhenAsync( + async (_, ct) => + { + return await applicants.IsApplicantNonResidentByUserId(userIdProvider.GetUserId(), ct); + }, + () => + { + RuleFor(r => r.ReentryPermit) + .NotEmpty() + .WithMessage("Non-residents must provide re-entry permission") + .SetValidator(reentryPermitValidator); + }); + + RuleFor(r => r.DestinationCountry) + .NotEmpty() + .WithMessage("Destination country can not be empty"); + + RuleFor(r => r.VisaCategory) + .IsInEnum(); + + RuleFor(r => r.RequestedNumberOfEntries) + .IsInEnum(); + + RuleFor(r => r.ValidDaysRequested) + .NotEmpty() + .WithMessage("Valid days requested can not be empty") + .GreaterThan(0) + .WithMessage($"Valid days requested should be positive number and less than {ConfigurationConstraints.MaxValidDays}") + .LessThanOrEqualTo(ConfigurationConstraints.MaxValidDays) + .WithMessage($"Valid days requested must be less than or equal to {ConfigurationConstraints.MaxValidDays}"); + + RuleForEach(r => r.PastVisas) + .SetValidator(pastVisaValidator); + + When(r => r.VisaCategory == VisaCategory.Transit, + () => + RuleFor(r => r.PermissionToDestCountry) + .SetValidator(permissionToDestCountryValidator)); + + RuleForEach(r => r.PastVisits) + .SetValidator(pastVisitValidator); + } + } +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/VisaApplicationCreateRequest.cs b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/VisaApplicationCreateRequest.cs index 58f8c1d..caf5207 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/VisaApplicationCreateRequest.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/VisaApplicationCreateRequest.cs @@ -4,7 +4,7 @@ namespace ApplicationLayer.Services.VisaApplications.Requests; /// Model of visa request from user public record VisaApplicationCreateRequest( - ReentryPermit ReentryPermit, + ReentryPermit? ReentryPermit, string DestinationCountry, VisaCategory VisaCategory, bool IsForGroup, diff --git a/SchengenVisaApi/Infrastructure/Database/ConfigurationConstraints.cs b/SchengenVisaApi/Domains/ConfigurationConstraints.cs similarity index 72% rename from SchengenVisaApi/Infrastructure/Database/ConfigurationConstraints.cs rename to SchengenVisaApi/Domains/ConfigurationConstraints.cs index 5f5816b..7ce2f72 100644 --- a/SchengenVisaApi/Infrastructure/Database/ConfigurationConstraints.cs +++ b/SchengenVisaApi/Domains/ConfigurationConstraints.cs @@ -1,4 +1,4 @@ -namespace Infrastructure.Database +namespace Domains { public static class ConfigurationConstraints { @@ -13,8 +13,12 @@ public const int NameLength = 50; public const int BuildingNumberLength = 10; public const int PassportNumberLength = 20; - public const int PhoneNumberLength = 15; + public const int PhoneNumberLength = 13; + public const int PhoneNumberMinLength = 11; public const int EmailLength = 254; public const int PasswordLength = 50; + public const int ApplicantMinAge = 14; + public const int JobTitleLength = 50; + public const int MaxValidDays = 90; } } diff --git a/SchengenVisaApi/Infrastructure/Automapper/Profiles/ApplicantProfile.cs b/SchengenVisaApi/Infrastructure/Automapper/Profiles/ApplicantProfile.cs index c0f3d5c..5344f7a 100644 --- a/SchengenVisaApi/Infrastructure/Automapper/Profiles/ApplicantProfile.cs +++ b/SchengenVisaApi/Infrastructure/Automapper/Profiles/ApplicantProfile.cs @@ -1,4 +1,5 @@ using ApplicationLayer.Services.Applicants.Models; +using ApplicationLayer.Services.AuthServices.Requests; using AutoMapper; using Domains.ApplicantDomain; @@ -9,6 +10,11 @@ namespace Infrastructure.Automapper.Profiles public ApplicantProfile() { CreateMap(MemberList.Destination); + + CreateMap(MemberList.Destination) + .ForMember(a => a.UserId, opts => opts.Ignore()) + .ForMember(a => a.Name, + opts => opts.MapFrom(r => r.ApplicantName)); } } } diff --git a/SchengenVisaApi/Infrastructure/Automapper/Profiles/PlaceOfWorkProfile.cs b/SchengenVisaApi/Infrastructure/Automapper/Profiles/PlaceOfWorkProfile.cs new file mode 100644 index 0000000..56a27a0 --- /dev/null +++ b/SchengenVisaApi/Infrastructure/Automapper/Profiles/PlaceOfWorkProfile.cs @@ -0,0 +1,16 @@ +using ApplicationLayer.Services.Applicants.Models; +using AutoMapper; +using Domains.ApplicantDomain; + +namespace Infrastructure.Automapper.Profiles +{ + public class PlaceOfWorkProfile : Profile + { + public PlaceOfWorkProfile() + { + CreateMap(MemberList.Destination) + .ForMember(p => p.Id, + opts => opts.UseDestinationValue()); + } + } +} diff --git a/SchengenVisaApi/Infrastructure/Automapper/Profiles/UserProfile.cs b/SchengenVisaApi/Infrastructure/Automapper/Profiles/UserProfile.cs index f995bfa..ce21f66 100644 --- a/SchengenVisaApi/Infrastructure/Automapper/Profiles/UserProfile.cs +++ b/SchengenVisaApi/Infrastructure/Automapper/Profiles/UserProfile.cs @@ -1,4 +1,4 @@ -using ApplicationLayer.Services.AuthServices.Requests; +using ApplicationLayer.Services.AuthServices.Common; using AutoMapper; using Domains.Users; @@ -8,11 +8,7 @@ namespace Infrastructure.Automapper.Profiles { public UserProfile() { - CreateMap(MemberList.Destination) - .ForMember(u => u.Role, - opts => opts.Ignore()); - - CreateMap() + CreateMap(MemberList.Destination) .ForMember(u => u.Role, opts => opts.Ignore()); } diff --git a/SchengenVisaApi/Infrastructure/Common/UserIdProvider.cs b/SchengenVisaApi/Infrastructure/Common/UserIdProvider.cs new file mode 100644 index 0000000..06e581f --- /dev/null +++ b/SchengenVisaApi/Infrastructure/Common/UserIdProvider.cs @@ -0,0 +1,19 @@ +using System.Security.Claims; +using ApplicationLayer.InfrastructureServicesInterfaces; +using Microsoft.AspNetCore.Http; + +namespace Infrastructure.Common +{ + public class UserIdProvider(IHttpContextAccessor contextAccessor) : IUserIdProvider + { + Guid IUserIdProvider.GetUserId() + { + var claim = contextAccessor.HttpContext!.User.Claims.SingleOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier); + if (claim is null) + { + throw new InvalidOperationException("UserIdProvider call for request with no authorization"); + } + return Guid.Parse(claim.Value); + } + } +} diff --git a/SchengenVisaApi/Infrastructure/Database/Applicants/Configuration/ApplicantConfiguration.cs b/SchengenVisaApi/Infrastructure/Database/Applicants/Configuration/ApplicantConfiguration.cs index dd8469f..aa5a299 100644 --- a/SchengenVisaApi/Infrastructure/Database/Applicants/Configuration/ApplicantConfiguration.cs +++ b/SchengenVisaApi/Infrastructure/Database/Applicants/Configuration/ApplicantConfiguration.cs @@ -1,4 +1,5 @@ -using Domains.ApplicantDomain; +using Domains; +using Domains.ApplicantDomain; using Domains.Users; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -31,5 +32,9 @@ public class ApplicantConfiguration : IEntityTypeConfiguration entity.Property(a => a.CityOfBirth) .IsUnicode(false) .HasMaxLength(ConfigurationConstraints.CityNameLength); + + entity.Property(a => a.JobTitle) + .IsUnicode(false) + .HasMaxLength(ConfigurationConstraints.JobTitleLength); } } diff --git a/SchengenVisaApi/Infrastructure/Database/Applicants/Configuration/PlaceOfWorkConfiguration.cs b/SchengenVisaApi/Infrastructure/Database/Applicants/Configuration/PlaceOfWorkConfiguration.cs index e4eda3f..8dd8bbe 100644 --- a/SchengenVisaApi/Infrastructure/Database/Applicants/Configuration/PlaceOfWorkConfiguration.cs +++ b/SchengenVisaApi/Infrastructure/Database/Applicants/Configuration/PlaceOfWorkConfiguration.cs @@ -1,4 +1,5 @@ -using Domains.ApplicantDomain; +using Domains; +using Domains.ApplicantDomain; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/SchengenVisaApi/Infrastructure/Database/Applicants/Repositories/ApplicantsRepository.cs b/SchengenVisaApi/Infrastructure/Database/Applicants/Repositories/ApplicantsRepository.cs index 64f4e7f..1748f2a 100644 --- a/SchengenVisaApi/Infrastructure/Database/Applicants/Repositories/ApplicantsRepository.cs +++ b/SchengenVisaApi/Infrastructure/Database/Applicants/Repositories/ApplicantsRepository.cs @@ -18,7 +18,8 @@ public sealed class ApplicantsRepository(IGenericReader reader, IGenericWriter w .Include(a => a.PlaceOfWork); } - async Task IApplicantsRepository.FindByUserIdAsync(Guid userId, CancellationToken cancellationToken) + /// + public async Task FindByUserIdAsync(Guid userId, CancellationToken cancellationToken) { var result = await LoadDomain().SingleOrDefaultAsync(a => a.UserId == userId, cancellationToken); return result ?? throw new ApplicantNotFoundByUserIdException(); @@ -29,4 +30,10 @@ public sealed class ApplicantsRepository(IGenericReader reader, IGenericWriter w var result = await base.LoadDomain().SingleOrDefaultAsync(a => a.UserId == userId, cancellationToken); return result?.Id ?? throw new ApplicantNotFoundByUserIdException(); } + + async Task IApplicantsRepository.IsApplicantNonResidentByUserId(Guid userId, CancellationToken cancellationToken) + { + var applicant = await FindByUserIdAsync(userId, cancellationToken); + return applicant.IsNonResident; + } } diff --git a/SchengenVisaApi/Infrastructure/Database/Users/Configuration/UserConfiguration.cs b/SchengenVisaApi/Infrastructure/Database/Users/Configuration/UserConfiguration.cs index 727ae64..d61c81b 100644 --- a/SchengenVisaApi/Infrastructure/Database/Users/Configuration/UserConfiguration.cs +++ b/SchengenVisaApi/Infrastructure/Database/Users/Configuration/UserConfiguration.cs @@ -1,4 +1,5 @@ -using Domains.Users; +using Domains; +using Domains.Users; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/SchengenVisaApi/Infrastructure/Database/VisaApplications/Configuration/VisaApplicationConfiguration.cs b/SchengenVisaApi/Infrastructure/Database/VisaApplications/Configuration/VisaApplicationConfiguration.cs index 39a69d6..d7278d7 100644 --- a/SchengenVisaApi/Infrastructure/Database/VisaApplications/Configuration/VisaApplicationConfiguration.cs +++ b/SchengenVisaApi/Infrastructure/Database/VisaApplications/Configuration/VisaApplicationConfiguration.cs @@ -1,4 +1,5 @@ -using Domains.ApplicantDomain; +using Domains; +using Domains.ApplicantDomain; using Domains.VisaApplicationDomain; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/SchengenVisaApi/Infrastructure/DependencyInjection.cs b/SchengenVisaApi/Infrastructure/DependencyInjection.cs index 300168a..afbfadd 100644 --- a/SchengenVisaApi/Infrastructure/DependencyInjection.cs +++ b/SchengenVisaApi/Infrastructure/DependencyInjection.cs @@ -38,6 +38,9 @@ public static class DependencyInjection services.AddSingleton(); + services.AddHttpContextAccessor(); + services.AddScoped(); + services.AddAutoMapper(Assembly.GetExecutingAssembly()); return services; diff --git a/SchengenVisaApi/SchengenVisaApi/Controllers/UsersController.cs b/SchengenVisaApi/SchengenVisaApi/Controllers/UsersController.cs index 2bf2f55..a32ee36 100644 --- a/SchengenVisaApi/SchengenVisaApi/Controllers/UsersController.cs +++ b/SchengenVisaApi/SchengenVisaApi/Controllers/UsersController.cs @@ -1,8 +1,11 @@ -using ApplicationLayer.Services.ApprovingAuthorities; +using ApplicationLayer.Services.AuthServices.Common; using ApplicationLayer.Services.AuthServices.LoginService; using ApplicationLayer.Services.AuthServices.RegisterService; using ApplicationLayer.Services.AuthServices.Requests; +using ApplicationLayer.Services.Users; +using ApplicationLayer.Services.Users.Requests; using Domains.Users; +using FluentValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SchengenVisaApi.Common; @@ -15,7 +18,9 @@ namespace SchengenVisaApi.Controllers public class UsersController( IRegisterService registerService, ILoginService loginService, - IUsersService authorityService) : VisaApiControllerBase + IUsersService usersService, + IValidator registerApplicantRequestValidator, + IValidator authDataValidator) : ControllerBase { /// Adds applicant with user account to DB [HttpPost] @@ -24,6 +29,8 @@ namespace SchengenVisaApi.Controllers [Route("register")] public async Task Register(RegisterApplicantRequest request, CancellationToken cancellationToken) { + await registerApplicantRequestValidator.ValidateAndThrowAsync(request, cancellationToken); + await registerService.RegisterApplicant(request, cancellationToken); return Ok(); } @@ -39,6 +46,8 @@ namespace SchengenVisaApi.Controllers [Authorize(policy: PolicyConstants.AdminPolicy)] public async Task RegisterAuthority(RegisterRequest request, CancellationToken cancellationToken) { + await authDataValidator.ValidateAndThrowAsync(request.AuthData, cancellationToken); + await registerService.RegisterAuthority(request, cancellationToken); return Ok(); } @@ -50,7 +59,7 @@ namespace SchengenVisaApi.Controllers [Route("login")] public async Task Login(string email, string password, CancellationToken cancellationToken) { - var result = await loginService.LoginAsync(new UserLoginRequest(email, password), cancellationToken); + var result = await loginService.LoginAsync(email, password, cancellationToken); return Ok(result); } @@ -62,10 +71,9 @@ namespace SchengenVisaApi.Controllers [ProducesResponseType(StatusCodes.Status401Unauthorized)] [Route("authorities")] [Authorize(policy: PolicyConstants.AdminPolicy)] - //todo return models public async Task GetAuthorityAccounts(CancellationToken cancellationToken) { - var result = await authorityService.GetAuthoritiesAccountsAsync(cancellationToken); + var result = await usersService.GetAuthoritiesAccountsAsync(cancellationToken); return Ok(result); } @@ -78,10 +86,11 @@ namespace SchengenVisaApi.Controllers [ProducesResponseType(StatusCodes.Status401Unauthorized)] [Route("authorities/{authorityAccountId:guid}")] [Authorize(policy: PolicyConstants.AdminPolicy)] - //todo replace args with ChangeAuthorityAuthDataRequest or something - public async Task ChangeAuthorityAuthData(Guid authorityAccountId, RegisterRequest authData, CancellationToken cancellationToken) + public async Task ChangeAuthorityAuthData(Guid authorityAccountId, AuthData authData, CancellationToken cancellationToken) { - await authorityService.ChangeAccountAuthDataAsync(authorityAccountId, authData, cancellationToken); + await authDataValidator.ValidateAndThrowAsync(authData, cancellationToken); + + await usersService.ChangeAccountAuthDataAsync(new ChangeUserAuthDataRequest(authorityAccountId, authData), cancellationToken); return Ok(); } @@ -96,7 +105,7 @@ namespace SchengenVisaApi.Controllers [Authorize(policy: PolicyConstants.AdminPolicy)] public async Task RemoveAuthorityAccount(Guid authorityAccountId, CancellationToken cancellationToken) { - await authorityService.RemoveUserAccount(authorityAccountId, cancellationToken); + await usersService.RemoveUserAccount(authorityAccountId, cancellationToken); return Ok(); } } diff --git a/SchengenVisaApi/SchengenVisaApi/Controllers/VisaApiControllerBase.cs b/SchengenVisaApi/SchengenVisaApi/Controllers/VisaApiControllerBase.cs deleted file mode 100644 index 6e90a82..0000000 --- a/SchengenVisaApi/SchengenVisaApi/Controllers/VisaApiControllerBase.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Security.Claims; -using Microsoft.AspNetCore.Mvc; - -namespace SchengenVisaApi.Controllers -{ - /// Base controller class for api controllers in project - public abstract class VisaApiControllerBase : ControllerBase - { - /// Returns identifier of authenticated user - protected Guid GetUserId() => Guid.Parse(HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value); - } -} diff --git a/SchengenVisaApi/SchengenVisaApi/Controllers/VisaApplicationController.cs b/SchengenVisaApi/SchengenVisaApi/Controllers/VisaApplicationController.cs index 391972e..09afbff 100644 --- a/SchengenVisaApi/SchengenVisaApi/Controllers/VisaApplicationController.cs +++ b/SchengenVisaApi/SchengenVisaApi/Controllers/VisaApplicationController.cs @@ -1,3 +1,4 @@ +using ApplicationLayer.InfrastructureServicesInterfaces; using ApplicationLayer.Services.VisaApplications.Handlers; using ApplicationLayer.Services.VisaApplications.Models; using ApplicationLayer.Services.VisaApplications.Requests; @@ -10,7 +11,9 @@ namespace SchengenVisaApi.Controllers; /// Controller for [ApiController] [Route("visaApplications")] -public class VisaApplicationController(IVisaApplicationRequestsHandler visaApplicationRequestsHandler) : VisaApiControllerBase +public class VisaApplicationController( + IVisaApplicationRequestsHandler visaApplicationRequestsHandler, + IUserIdProvider userIdProvider) : ControllerBase { /// Returns all applications from DB /// Accessible only for approving authorities @@ -36,7 +39,7 @@ public class VisaApplicationController(IVisaApplicationRequestsHandler visaAppli [Route("OfApplicant")] public async Task GetForApplicant(CancellationToken cancellationToken) { - var userId = GetUserId(); + var userId = userIdProvider.GetUserId(); var result = await visaApplicationRequestsHandler.GetForApplicantAsync(userId, cancellationToken); return Ok(result); } @@ -51,7 +54,7 @@ public class VisaApplicationController(IVisaApplicationRequestsHandler visaAppli [Authorize(policy: PolicyConstants.ApplicantPolicy)] public async Task Create(VisaApplicationCreateRequest request, CancellationToken cancellationToken) { - var userId = GetUserId(); + var userId = userIdProvider.GetUserId(); await visaApplicationRequestsHandler.HandleCreateRequestAsync(userId, request, cancellationToken); return Ok(); } @@ -67,7 +70,7 @@ public class VisaApplicationController(IVisaApplicationRequestsHandler visaAppli [Route("{applicationId:guid}")] public async Task CloseApplication(Guid applicationId, CancellationToken cancellationToken) { - var userId = GetUserId(); + var userId = userIdProvider.GetUserId(); await visaApplicationRequestsHandler.HandleCloseRequestAsync(userId, applicationId, cancellationToken); return Ok(); } diff --git a/SchengenVisaApi/SchengenVisaApi/ExceptionFilters/GlobalExceptionsFilter.cs b/SchengenVisaApi/SchengenVisaApi/ExceptionFilters/GlobalExceptionsFilter.cs index 4778f64..5c398ac 100644 --- a/SchengenVisaApi/SchengenVisaApi/ExceptionFilters/GlobalExceptionsFilter.cs +++ b/SchengenVisaApi/SchengenVisaApi/ExceptionFilters/GlobalExceptionsFilter.cs @@ -2,6 +2,7 @@ using ApplicationLayer.Services.AuthServices.LoginService.Exceptions; using ApplicationLayer.Services.GeneralExceptions; using ApplicationLayer.Services.VisaApplications.Exceptions; +using FluentValidation; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -16,43 +17,52 @@ namespace SchengenVisaApi.ExceptionFilters var exception = context.Exception; var problemDetails = new ProblemDetails(); - if (exception is ApiException) + switch (exception) { - problemDetails.Detail = exception.Message; - switch (exception) - { - case EntityNotFoundException: - problemDetails.Status = StatusCodes.Status404NotFound; - problemDetails.Title = "Requested entity not found"; - problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4"; - break; - case IncorrectLoginDataException: - problemDetails.Status = StatusCodes.Status403Forbidden; - problemDetails.Title = "Auth failed"; - problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.3"; - break; - case AlreadyExistsException: - problemDetails.Status = StatusCodes.Status409Conflict; - problemDetails.Title = "Already exists"; - problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.8"; - break; - case ApplicationAlreadyProcessedException: - problemDetails.Status = StatusCodes.Status409Conflict; - problemDetails.Title = "Already processed"; - problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.8"; - break; - default: - problemDetails.Status = StatusCodes.Status400BadRequest; - problemDetails.Title = "Bad request"; - problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1"; - break; - } - } - else - { - problemDetails.Status = StatusCodes.Status500InternalServerError; - problemDetails.Title = "An unhandled error occured"; - problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1"; + case ValidationException validationException: + problemDetails.Extensions.Add("Errors", validationException.Errors.Select(e => e.ErrorMessage)); + problemDetails.Detail = "Validation errors occured"; + problemDetails.Status = StatusCodes.Status400BadRequest; + problemDetails.Title = "Bad request"; + problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1"; + break; + case ApiException: + problemDetails.Detail = exception.Message; + switch (exception) + { + case EntityNotFoundException: + problemDetails.Status = StatusCodes.Status404NotFound; + problemDetails.Title = "Requested entity not found"; + problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4"; + break; + case IncorrectLoginDataException: + problemDetails.Status = StatusCodes.Status403Forbidden; + problemDetails.Title = "Auth failed"; + problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.3"; + break; + case AlreadyExistsException: + problemDetails.Status = StatusCodes.Status409Conflict; + problemDetails.Title = "Already exists"; + problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.8"; + break; + case ApplicationAlreadyProcessedException: + problemDetails.Status = StatusCodes.Status409Conflict; + problemDetails.Title = "Already processed"; + problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.8"; + break; + default: + problemDetails.Status = StatusCodes.Status400BadRequest; + problemDetails.Title = "Bad request"; + problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1"; + break; + } + + break; + default: + problemDetails.Status = StatusCodes.Status500InternalServerError; + problemDetails.Title = "An unhandled error occured"; + problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1"; + break; } await Results.Problem(problemDetails).ExecuteAsync(context.HttpContext);