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 deleted file mode 100644 index 98cf71a..0000000 --- a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/Exceptions/UserAlreadyExistsException.cs +++ /dev/null @@ -1,7 +0,0 @@ -using ApplicationLayer.GeneralExceptions; -using ApplicationLayer.Services.AuthServices.Requests; - -namespace ApplicationLayer.Services.AuthServices.RegisterService.Exceptions -{ - public class UserAlreadyExistsException(RegisterRequest request) : AlreadyExistsException($"User with email '{request.Email}' already exists"); -} diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/IRegisterService.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/IRegisterService.cs index 5d1f407..52d69e4 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/IRegisterService.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/IRegisterService.cs @@ -6,7 +6,7 @@ namespace ApplicationLayer.Services.AuthServices.RegisterService public interface IRegisterService { /// Handle - Task Register(RegisterApplicantRequest request, CancellationToken cancellationToken); + Task RegisterApplicant(RegisterApplicantRequest request, CancellationToken cancellationToken); /// Handles and adds approving authority account Task RegisterAuthority(RegisterRequest request, CancellationToken cancellationToken); diff --git a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/RegisterService.cs b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/RegisterService.cs index 68cedc8..86be1f9 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/RegisterService.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/AuthServices/RegisterService/RegisterService.cs @@ -1,8 +1,8 @@ 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; using Domains.Users; @@ -12,37 +12,16 @@ namespace ApplicationLayer.Services.AuthServices.RegisterService public class RegisterService( IUsersRepository users, IApplicantsRepository applicants, - IUnitOfWork unitOfWork) : IRegisterService + IUnitOfWork unitOfWork, + IMapper mapper) : IRegisterService { - async Task IRegisterService.Register(RegisterApplicantRequest request, CancellationToken cancellationToken) + 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.AuthData); + user.Role = Role.Applicant; - //TODO mapper - var user = new User { Email = request.Email, Password = request.Password, 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); @@ -52,14 +31,8 @@ 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); - } - - //TODO mapper - var user = new User { Email = request.Email, Password = request.Password, Role = Role.ApprovingAuthority }; + 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/GeneralExceptions/EntityUsedInDatabaseException.cs b/SchengenVisaApi/ApplicationLayer/Services/GeneralExceptions/EntityUsedInDatabaseException.cs deleted file mode 100644 index 74c0a55..0000000 --- a/SchengenVisaApi/ApplicationLayer/Services/GeneralExceptions/EntityUsedInDatabaseException.cs +++ /dev/null @@ -1,7 +0,0 @@ -using ApplicationLayer.GeneralExceptions; - -namespace ApplicationLayer.Services.GeneralExceptions -{ - /// Exception to throw when can't complete some action on entity(delete or something) because it's needed for other entities - public class EntityUsedInDatabaseException(string message) : ApiException(message); -} 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/Handlers/VisaApplicationRequestsHandler.cs b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Handlers/VisaApplicationRequestsHandler.cs index 88a6b27..a44b8b3 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Handlers/VisaApplicationRequestsHandler.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Handlers/VisaApplicationRequestsHandler.cs @@ -5,6 +5,7 @@ using ApplicationLayer.Services.VisaApplications.Exceptions; using ApplicationLayer.Services.VisaApplications.Models; using ApplicationLayer.Services.VisaApplications.NeededServices; using ApplicationLayer.Services.VisaApplications.Requests; +using AutoMapper; using Domains.VisaApplicationDomain; namespace ApplicationLayer.Services.VisaApplications.Handlers; @@ -13,13 +14,14 @@ namespace ApplicationLayer.Services.VisaApplications.Handlers; public class VisaApplicationRequestsHandler( IVisaApplicationsRepository applications, IApplicantsRepository applicants, - IUnitOfWork unitOfWork) : IVisaApplicationRequestsHandler + IUnitOfWork unitOfWork, + IMapper mapper, + IDateTimeProvider dateTimeProvider) : IVisaApplicationRequestsHandler { async Task> IVisaApplicationRequestsHandler.GetAllAsync(CancellationToken cancellationToken) { var applicationsList = await applications.GetAllAsync(cancellationToken); - //todo mapper var applicationModels = applicationsList .Select(a => MapVisaApplicationToModelForAuthorities(a, cancellationToken).Result) .ToList(); @@ -30,85 +32,28 @@ public class VisaApplicationRequestsHandler( CancellationToken cancellationToken) { var applicant = await applicants.GetByIdAsync(visaApplication.ApplicantId, cancellationToken); - var applicantModel = new ApplicantModel - { - Citizenship = applicant.Citizenship, - Gender = applicant.Gender, - Name = applicant.Name, - Passport = applicant.Passport, - BirthDate = applicant.BirthDate, - FatherName = applicant.FatherName, - JobTitle = applicant.JobTitle, - MaritalStatus = applicant.MaritalStatus, - MotherName = applicant.MotherName, - CitizenshipByBirth = applicant.CitizenshipByBirth, - CityOfBirth = applicant.CityOfBirth, - CountryOfBirth = applicant.CountryOfBirth, - IsNonResident = applicant.IsNonResident, - PlaceOfWork = applicant.PlaceOfWork, - }; - return new VisaApplicationModelForAuthority - { - PastVisits = visaApplication.PastVisits, - ReentryPermit = visaApplication.ReentryPermit, - VisaCategory = visaApplication.VisaCategory, - PermissionToDestCountry = visaApplication.PermissionToDestCountry, - DestinationCountry = visaApplication.DestinationCountry, - PastVisas = visaApplication.PastVisas, - RequestDate = visaApplication.RequestDate, - ValidDaysRequested = visaApplication.ValidDaysRequested, - RequestedNumberOfEntries = visaApplication.RequestedNumberOfEntries, - ForGroup = visaApplication.ForGroup, - Applicant = applicantModel, - Id = visaApplication.Id, - Status = visaApplication.Status - }; + var applicantModel = mapper.Map(applicant); + + var model = mapper.Map(visaApplication); + model.Applicant = applicantModel; + + return model; } public async Task> GetForApplicantAsync(Guid userId, CancellationToken cancellationToken) { - //todo mapper var applicantId = await applicants.GetApplicantIdByUserId(userId, cancellationToken); var visaApplications = await applications.GetOfApplicantAsync(applicantId, cancellationToken); - return visaApplications.Select(va => new VisaApplicationModelForApplicant - { - DestinationCountry = va.DestinationCountry, - ValidDaysRequested = va.ValidDaysRequested, - ReentryPermit = va.ReentryPermit, - VisaCategory = va.VisaCategory, - RequestedNumberOfEntries = va.RequestedNumberOfEntries, - PermissionToDestCountry = va.PermissionToDestCountry, - ForGroup = va.ForGroup, - PastVisas = va.PastVisas, - RequestDate = va.RequestDate, - PastVisits = va.PastVisits, - Id = va.Id, - Status = va.Status - }) - .ToList(); + return mapper.Map>(visaApplications); } public async Task HandleCreateRequestAsync(Guid userId, VisaApplicationCreateRequest request, CancellationToken cancellationToken) { - //TODO mapper - var applicant = await applicants.FindByUserIdAsync(userId, cancellationToken); - var visaApplication = new VisaApplication - { - ApplicantId = applicant.Id, - RequestedNumberOfEntries = request.RequestedNumberOfEntries, - ValidDaysRequested = request.ValidDaysRequested, - ReentryPermit = request.ReentryPermit, - VisaCategory = request.VisaCategory, - PermissionToDestCountry = request.PermissionToDestCountry, - DestinationCountry = request.DestinationCountry, - PastVisas = request.PastVisas.ToList(), - PastVisits = request.PastVisits.ToList(), - ForGroup = request.IsForGroup, - RequestDate = DateTime.Today, - Status = ApplicationStatus.Pending - }; + var visaApplication = mapper.Map(request); + visaApplication.RequestDate = dateTimeProvider.Now(); + visaApplication.ApplicantId = applicant.Id; await applications.AddAsync(visaApplication, cancellationToken); @@ -134,11 +79,9 @@ public class VisaApplicationRequestsHandler( var application = await applications.GetByIdAsync(applicationId, cancellationToken); if (application.Status != ApplicationStatus.Pending) { - //todo refactor exceptions throw new ApplicationAlreadyProcessedException(); } - //todo mapper ApplicationStatus statusToSet = status switch { AuthorityRequestStatuses.Approved => ApplicationStatus.Approved, 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..60be0f4 --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/VisaApplications/Requests/Validation/VisaApplicationCreateRequestValidator.cs @@ -0,0 +1,54 @@ +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) + { + RuleFor(r => r.ReentryPermit) + .NotEmpty() + .WithMessage("Non-residents must provide re-entry permission") + .SetValidator(reentryPermitValidator) + .WhenAsync(async (r, ct) => + await applicants.IsApplicantNonResidentByUserId(userIdProvider.GetUserId(), ct)); + + 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) + .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/Domains/Domains.csproj b/SchengenVisaApi/Domains/Domains.csproj index 3a63532..c6dfae8 100644 --- a/SchengenVisaApi/Domains/Domains.csproj +++ b/SchengenVisaApi/Domains/Domains.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/SchengenVisaApi/Infrastructure/Automapper/Profiles/ApplicantProfile.cs b/SchengenVisaApi/Infrastructure/Automapper/Profiles/ApplicantProfile.cs new file mode 100644 index 0000000..5344f7a --- /dev/null +++ b/SchengenVisaApi/Infrastructure/Automapper/Profiles/ApplicantProfile.cs @@ -0,0 +1,20 @@ +using ApplicationLayer.Services.Applicants.Models; +using ApplicationLayer.Services.AuthServices.Requests; +using AutoMapper; +using Domains.ApplicantDomain; + +namespace Infrastructure.Automapper.Profiles +{ + public class ApplicantProfile : Profile + { + 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 new file mode 100644 index 0000000..ce21f66 --- /dev/null +++ b/SchengenVisaApi/Infrastructure/Automapper/Profiles/UserProfile.cs @@ -0,0 +1,16 @@ +using ApplicationLayer.Services.AuthServices.Common; +using AutoMapper; +using Domains.Users; + +namespace Infrastructure.Automapper.Profiles +{ + public class UserProfile : Profile + { + public UserProfile() + { + CreateMap(MemberList.Destination) + .ForMember(u => u.Role, + opts => opts.Ignore()); + } + } +} diff --git a/SchengenVisaApi/Infrastructure/Automapper/Profiles/VisaApplicationProfile.cs b/SchengenVisaApi/Infrastructure/Automapper/Profiles/VisaApplicationProfile.cs new file mode 100644 index 0000000..d274a03 --- /dev/null +++ b/SchengenVisaApi/Infrastructure/Automapper/Profiles/VisaApplicationProfile.cs @@ -0,0 +1,25 @@ +using ApplicationLayer.Services.VisaApplications.Models; +using ApplicationLayer.Services.VisaApplications.Requests; +using AutoMapper; +using Domains.VisaApplicationDomain; + +namespace Infrastructure.Automapper.Profiles +{ + public class VisaApplicationProfile : Profile + { + public VisaApplicationProfile() + { + CreateMap(MemberList.Destination); + + CreateMap(MemberList.Destination) + .ForMember(model => model.Applicant, + opts => opts.Ignore()); + + CreateMap(MemberList.Destination) + .ForMember(va => va.RequestDate, + opts => opts.Ignore()) + .ForMember(va => va.ApplicantId, + 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 a05b816..afbfadd 100644 --- a/SchengenVisaApi/Infrastructure/DependencyInjection.cs +++ b/SchengenVisaApi/Infrastructure/DependencyInjection.cs @@ -1,4 +1,5 @@ -using ApplicationLayer.InfrastructureServicesInterfaces; +using System.Reflection; +using ApplicationLayer.InfrastructureServicesInterfaces; using ApplicationLayer.Services.Applicants.NeededServices; using ApplicationLayer.Services.AuthServices.NeededServices; using ApplicationLayer.Services.VisaApplications.NeededServices; @@ -37,6 +38,11 @@ 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 ac0201b..45093fa 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,16 +18,21 @@ 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] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [Route("register")] public async Task Register(RegisterApplicantRequest request, CancellationToken cancellationToken) { - await registerService.Register(request, cancellationToken); + await registerApplicantRequestValidator.ValidateAndThrowAsync(request, cancellationToken); + + await registerService.RegisterApplicant(request, cancellationToken); return Ok(); } @@ -35,10 +43,13 @@ namespace SchengenVisaApi.Controllers [ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [Route("authorities")] [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 +61,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); } @@ -64,7 +75,7 @@ namespace SchengenVisaApi.Controllers [Authorize(policy: PolicyConstants.AdminPolicy)] public async Task GetAuthorityAccounts(CancellationToken cancellationToken) { - var result = await authorityService.GetAuthoritiesAccountsAsync(cancellationToken); + var result = await usersService.GetAuthoritiesAccountsAsync(cancellationToken); return Ok(result); } @@ -75,12 +86,14 @@ namespace SchengenVisaApi.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [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(); } @@ -95,7 +108,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..e138f55 100644 --- a/SchengenVisaApi/SchengenVisaApi/Controllers/VisaApplicationController.cs +++ b/SchengenVisaApi/SchengenVisaApi/Controllers/VisaApplicationController.cs @@ -1,6 +1,8 @@ +using ApplicationLayer.InfrastructureServicesInterfaces; using ApplicationLayer.Services.VisaApplications.Handlers; using ApplicationLayer.Services.VisaApplications.Models; using ApplicationLayer.Services.VisaApplications.Requests; +using FluentValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SchengenVisaApi.Common; @@ -10,7 +12,10 @@ namespace SchengenVisaApi.Controllers; /// Controller for [ApiController] [Route("visaApplications")] -public class VisaApplicationController(IVisaApplicationRequestsHandler visaApplicationRequestsHandler) : VisaApiControllerBase +public class VisaApplicationController( + IVisaApplicationRequestsHandler visaApplicationRequestsHandler, + IUserIdProvider userIdProvider, + IValidator visaApplicationCreateRequestValidator) : ControllerBase { /// Returns all applications from DB /// Accessible only for approving authorities @@ -36,7 +41,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); } @@ -48,10 +53,13 @@ public class VisaApplicationController(IVisaApplicationRequestsHandler visaAppli [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [Authorize(policy: PolicyConstants.ApplicantPolicy)] public async Task Create(VisaApplicationCreateRequest request, CancellationToken cancellationToken) { - var userId = GetUserId(); + await visaApplicationCreateRequestValidator.ValidateAndThrowAsync(request, cancellationToken); + + var userId = userIdProvider.GetUserId(); await visaApplicationRequestsHandler.HandleCreateRequestAsync(userId, request, cancellationToken); return Ok(); } @@ -67,7 +75,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);