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);