Merge pull request #8 from prtsie/todos

Todos
This commit is contained in:
prtsie
2024-08-26 11:31:19 +03:00
committed by GitHub
48 changed files with 702 additions and 231 deletions

View File

@@ -11,6 +11,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation" Version="11.9.2" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0-preview.7.24405.7" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0-preview.7.24405.7" />
</ItemGroup> </ItemGroup>

View File

@@ -1,7 +1,9 @@
using ApplicationLayer.Services.ApprovingAuthorities; using System.Reflection;
using ApplicationLayer.Services.AuthServices.LoginService; using ApplicationLayer.Services.AuthServices.LoginService;
using ApplicationLayer.Services.AuthServices.RegisterService; using ApplicationLayer.Services.AuthServices.RegisterService;
using ApplicationLayer.Services.Users;
using ApplicationLayer.Services.VisaApplications.Handlers; using ApplicationLayer.Services.VisaApplications.Handlers;
using FluentValidation;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace ApplicationLayer; namespace ApplicationLayer;
@@ -12,6 +14,8 @@ public static class DependencyInjection
/// Add services for Application layer /// Add services for Application layer
public static IServiceCollection AddApplicationLayer(this IServiceCollection services, bool isDevelopment = false) public static IServiceCollection AddApplicationLayer(this IServiceCollection services, bool isDevelopment = false)
{ {
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddScoped<IVisaApplicationRequestsHandler, VisaApplicationRequestsHandler>(); services.AddScoped<IVisaApplicationRequestsHandler, VisaApplicationRequestsHandler>();
services.AddScoped<IRegisterService, RegisterService>(); services.AddScoped<IRegisterService, RegisterService>();

View File

@@ -0,0 +1,8 @@
namespace ApplicationLayer.InfrastructureServicesInterfaces
{
public interface IUserIdProvider
{
/// Returns identifier of authenticated user who sent the request
Guid GetUserId();
}
}

View File

@@ -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!;
}
}

View File

@@ -11,4 +11,7 @@ public interface IApplicantsRepository : IGenericRepository<Applicant>
/// Get identifier of applicant by user identifier /// Get identifier of applicant by user identifier
Task<Guid> GetApplicantIdByUserId(Guid userId, CancellationToken cancellationToken); Task<Guid> GetApplicantIdByUserId(Guid userId, CancellationToken cancellationToken);
/// Returns value of NonResident property of applicant
Task<bool> IsApplicantNonResidentByUserId(Guid userId, CancellationToken cancellationToken);
} }

View File

@@ -0,0 +1,4 @@
namespace ApplicationLayer.Services.AuthServices.Common
{
public record AuthData(string Email, string Password);
}

View File

@@ -1,23 +1,22 @@
using ApplicationLayer.Services.AuthServices.LoginService.Exceptions; using ApplicationLayer.Services.AuthServices.LoginService.Exceptions;
using ApplicationLayer.Services.AuthServices.NeededServices; using ApplicationLayer.Services.AuthServices.NeededServices;
using ApplicationLayer.Services.AuthServices.Requests;
using Domains.Users; using Domains.Users;
namespace ApplicationLayer.Services.AuthServices.LoginService namespace ApplicationLayer.Services.AuthServices.LoginService
{ {
public class DevelopmentLoginService(IUsersRepository users, ITokenGenerator tokenGenerator) : ILoginService public class DevelopmentLoginService(IUsersRepository users, ITokenGenerator tokenGenerator) : ILoginService
{ {
async Task<string> ILoginService.LoginAsync(UserLoginRequest request, CancellationToken cancellationToken) async Task<string> 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 }; var admin = new User { Role = Role.Admin };
return tokenGenerator.CreateToken(admin); return tokenGenerator.CreateToken(admin);
} }
var user = await users.FindByEmailAsync(request.Email, cancellationToken); var user = await users.FindByEmailAsync(email, cancellationToken);
if (user is null || user.Password != request.Password) if (user is null || user.Password != password)
{ {
throw new IncorrectLoginDataException(); throw new IncorrectLoginDataException();
} }

View File

@@ -1,12 +1,10 @@
using ApplicationLayer.Services.AuthServices.Requests; namespace ApplicationLayer.Services.AuthServices.LoginService
namespace ApplicationLayer.Services.AuthServices.LoginService
{ {
/// Handles <see cref="UserLoginRequest"/> /// Handles login requests
public interface ILoginService public interface ILoginService
{ {
/// Handle <see cref="UserLoginRequest"/> /// Handle login request
/// <returns>JWT-token</returns> /// <returns>JWT-token</returns>
Task<string> LoginAsync(UserLoginRequest request, CancellationToken cancellationToken); Task<string> LoginAsync(string email, string password, CancellationToken cancellationToken);
} }
} }

View File

@@ -1,16 +1,15 @@
using ApplicationLayer.Services.AuthServices.LoginService.Exceptions; using ApplicationLayer.Services.AuthServices.LoginService.Exceptions;
using ApplicationLayer.Services.AuthServices.NeededServices; using ApplicationLayer.Services.AuthServices.NeededServices;
using ApplicationLayer.Services.AuthServices.Requests;
namespace ApplicationLayer.Services.AuthServices.LoginService namespace ApplicationLayer.Services.AuthServices.LoginService
{ {
/// <inheritdoc cref="ILoginService"/> /// <inheritdoc cref="ILoginService"/>
public class LoginService(IUsersRepository users, ITokenGenerator tokenGenerator) : ILoginService public class LoginService(IUsersRepository users, ITokenGenerator tokenGenerator) : ILoginService
{ {
async Task<string> ILoginService.LoginAsync(UserLoginRequest request, CancellationToken cancellationToken) async Task<string> ILoginService.LoginAsync(string email, string password, CancellationToken cancellationToken)
{ {
var user = await users.FindByEmailAsync(request.Email, cancellationToken); var user = await users.FindByEmailAsync(email, cancellationToken);
if (user is null || user.Password != request.Password) if (user is null || user.Password != password)
{ {
throw new IncorrectLoginDataException(); throw new IncorrectLoginDataException();
} }

View File

@@ -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");
}

View File

@@ -6,7 +6,7 @@ namespace ApplicationLayer.Services.AuthServices.RegisterService
public interface IRegisterService public interface IRegisterService
{ {
/// Handle <see cref="RegisterApplicantRequest"/> /// Handle <see cref="RegisterApplicantRequest"/>
Task Register(RegisterApplicantRequest request, CancellationToken cancellationToken); Task RegisterApplicant(RegisterApplicantRequest request, CancellationToken cancellationToken);
/// Handles <see cref="RegisterRequest"/> and adds approving authority account /// Handles <see cref="RegisterRequest"/> and adds approving authority account
Task RegisterAuthority(RegisterRequest request, CancellationToken cancellationToken); Task RegisterAuthority(RegisterRequest request, CancellationToken cancellationToken);

View File

@@ -1,8 +1,8 @@
using ApplicationLayer.InfrastructureServicesInterfaces; using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.Applicants.NeededServices; using ApplicationLayer.Services.Applicants.NeededServices;
using ApplicationLayer.Services.AuthServices.NeededServices; using ApplicationLayer.Services.AuthServices.NeededServices;
using ApplicationLayer.Services.AuthServices.RegisterService.Exceptions;
using ApplicationLayer.Services.AuthServices.Requests; using ApplicationLayer.Services.AuthServices.Requests;
using AutoMapper;
using Domains.ApplicantDomain; using Domains.ApplicantDomain;
using Domains.Users; using Domains.Users;
@@ -12,37 +12,16 @@ namespace ApplicationLayer.Services.AuthServices.RegisterService
public class RegisterService( public class RegisterService(
IUsersRepository users, IUsersRepository users,
IApplicantsRepository applicants, 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 var user = mapper.Map<User>(request.AuthData);
if (await users.FindByEmailAsync(request.Email, cancellationToken) is not null) user.Role = Role.Applicant;
{
throw new UserAlreadyExistsException(request);
}
//TODO mapper var applicant = mapper.Map<Applicant>(request);
var user = new User { Email = request.Email, Password = request.Password, Role = Role.Applicant }; applicant.UserId = user.Id;
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
};
await users.AddAsync(user, cancellationToken); await users.AddAsync(user, cancellationToken);
await applicants.AddAsync(applicant, cancellationToken); await applicants.AddAsync(applicant, cancellationToken);
@@ -52,14 +31,8 @@ namespace ApplicationLayer.Services.AuthServices.RegisterService
async Task IRegisterService.RegisterAuthority(RegisterRequest request, CancellationToken cancellationToken) async Task IRegisterService.RegisterAuthority(RegisterRequest request, CancellationToken cancellationToken)
{ {
//todo move to validation layer var user = mapper.Map<User>(request.AuthData);
if (await users.FindByEmailAsync(request.Email, cancellationToken) is not null) user.Role = Role.ApprovingAuthority;
{
throw new UserAlreadyExistsException(request);
}
//TODO mapper
var user = new User { Email = request.Email, Password = request.Password, Role = Role.ApprovingAuthority };
await users.AddAsync(user, cancellationToken); await users.AddAsync(user, cancellationToken);

View File

@@ -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 namespace ApplicationLayer.Services.AuthServices.Requests
{ {
public record RegisterApplicantRequest( public record RegisterApplicantRequest(
string Email, AuthData AuthData,
string Password,
Name ApplicantName, Name ApplicantName,
Passport Passport, Passport Passport,
DateTime BirthDate, DateTime BirthDate,
@@ -17,6 +18,6 @@ namespace ApplicationLayer.Services.AuthServices.Requests
Name FatherName, Name FatherName,
Name MotherName, Name MotherName,
string JobTitle, string JobTitle,
PlaceOfWork PlaceOfWork, PlaceOfWorkModel PlaceOfWork,
bool IsNonResident) : RegisterRequest(Email, Password); bool IsNonResident) : RegisterRequest(AuthData);
} }

View File

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

View File

@@ -1,4 +0,0 @@
namespace ApplicationLayer.Services.AuthServices.Requests
{
public record UserLoginRequest(string Email, string Password);
}

View File

@@ -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<AuthData>
{
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}");
}
}
}

View File

@@ -0,0 +1,28 @@
using Domains;
using Domains.ApplicantDomain;
using FluentValidation;
namespace ApplicationLayer.Services.AuthServices.Requests.Validation
{
public class NameValidator : AbstractValidator<Name>
{
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}");
}
}
}

View File

@@ -0,0 +1,37 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains;
using Domains.ApplicantDomain;
using FluentValidation;
namespace ApplicationLayer.Services.AuthServices.Requests.Validation
{
public class PassportValidator : AbstractValidator<Passport>
{
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");
}
}
}

View File

@@ -0,0 +1,50 @@
using ApplicationLayer.Services.Applicants.Models;
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.AuthServices.Requests.Validation
{
public class PlaceOfWorkModelValidator : AbstractValidator<PlaceOfWorkModel>
{
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}");
}
}
}

View File

@@ -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<RegisterApplicantRequest>
{
public RegisterApplicantRequestValidator(
IDateTimeProvider dateTimeProvider,
IValidator<Name> nameValidator,
IValidator<AuthData> authDataValidator,
IValidator<Passport> passportValidator,
IValidator<PlaceOfWorkModel> 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);
}
}
}

View File

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

View File

@@ -1,7 +1,7 @@
using ApplicationLayer.Services.AuthServices.Requests; using ApplicationLayer.Services.Users.Requests;
using Domains.Users; using Domains.Users;
namespace ApplicationLayer.Services.ApprovingAuthorities namespace ApplicationLayer.Services.Users
{ {
/// user accounts service /// user accounts service
public interface IUsersService public interface IUsersService
@@ -11,10 +11,9 @@ namespace ApplicationLayer.Services.ApprovingAuthorities
Task<List<User>> GetAuthoritiesAccountsAsync(CancellationToken cancellationToken); Task<List<User>> GetAuthoritiesAccountsAsync(CancellationToken cancellationToken);
/// Changes authentication data for an account /// Changes authentication data for an account
/// <param name="userId">identifier of account</param> /// <param name="request"> Request object with identifier of user and new authentication data</param>
/// <param name="data">request data with new email and password</param>
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
Task ChangeAccountAuthDataAsync(Guid userId, RegisterRequest data, CancellationToken cancellationToken); Task ChangeAccountAuthDataAsync(ChangeUserAuthDataRequest request, CancellationToken cancellationToken);
/// Removes user account /// Removes user account
/// <param name="userId">Identifier of account</param> /// <param name="userId">Identifier of account</param>

View File

@@ -0,0 +1,6 @@
using ApplicationLayer.Services.AuthServices.Common;
namespace ApplicationLayer.Services.Users.Requests
{
public record ChangeUserAuthDataRequest(Guid UserId, AuthData NewAuthData);
}

View File

@@ -1,9 +1,9 @@
using ApplicationLayer.InfrastructureServicesInterfaces; using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.AuthServices.NeededServices; using ApplicationLayer.Services.AuthServices.NeededServices;
using ApplicationLayer.Services.AuthServices.Requests; using ApplicationLayer.Services.Users.Requests;
using Domains.Users; using Domains.Users;
namespace ApplicationLayer.Services.ApprovingAuthorities namespace ApplicationLayer.Services.Users
{ {
public class UsersService(IUsersRepository users, IUnitOfWork unitOfWork) : IUsersService public class UsersService(IUsersRepository users, IUnitOfWork unitOfWork) : IUsersService
{ {
@@ -12,12 +12,12 @@ namespace ApplicationLayer.Services.ApprovingAuthorities
return await users.GetAllOfRoleAsync(Role.ApprovingAuthority, cancellationToken); 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.Email = request.NewAuthData.Email;
user.Password = data.Password; user.Password = request.NewAuthData.Password;
await users.UpdateAsync(user, cancellationToken); await users.UpdateAsync(user, cancellationToken);
await unitOfWork.SaveAsync(cancellationToken); await unitOfWork.SaveAsync(cancellationToken);

View File

@@ -5,6 +5,7 @@ using ApplicationLayer.Services.VisaApplications.Exceptions;
using ApplicationLayer.Services.VisaApplications.Models; using ApplicationLayer.Services.VisaApplications.Models;
using ApplicationLayer.Services.VisaApplications.NeededServices; using ApplicationLayer.Services.VisaApplications.NeededServices;
using ApplicationLayer.Services.VisaApplications.Requests; using ApplicationLayer.Services.VisaApplications.Requests;
using AutoMapper;
using Domains.VisaApplicationDomain; using Domains.VisaApplicationDomain;
namespace ApplicationLayer.Services.VisaApplications.Handlers; namespace ApplicationLayer.Services.VisaApplications.Handlers;
@@ -13,13 +14,14 @@ namespace ApplicationLayer.Services.VisaApplications.Handlers;
public class VisaApplicationRequestsHandler( public class VisaApplicationRequestsHandler(
IVisaApplicationsRepository applications, IVisaApplicationsRepository applications,
IApplicantsRepository applicants, IApplicantsRepository applicants,
IUnitOfWork unitOfWork) : IVisaApplicationRequestsHandler IUnitOfWork unitOfWork,
IMapper mapper,
IDateTimeProvider dateTimeProvider) : IVisaApplicationRequestsHandler
{ {
async Task<List<VisaApplicationModelForAuthority>> IVisaApplicationRequestsHandler.GetAllAsync(CancellationToken cancellationToken) async Task<List<VisaApplicationModelForAuthority>> IVisaApplicationRequestsHandler.GetAllAsync(CancellationToken cancellationToken)
{ {
var applicationsList = await applications.GetAllAsync(cancellationToken); var applicationsList = await applications.GetAllAsync(cancellationToken);
//todo mapper
var applicationModels = applicationsList var applicationModels = applicationsList
.Select(a => MapVisaApplicationToModelForAuthorities(a, cancellationToken).Result) .Select(a => MapVisaApplicationToModelForAuthorities(a, cancellationToken).Result)
.ToList(); .ToList();
@@ -30,85 +32,28 @@ public class VisaApplicationRequestsHandler(
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var applicant = await applicants.GetByIdAsync(visaApplication.ApplicantId, cancellationToken); var applicant = await applicants.GetByIdAsync(visaApplication.ApplicantId, cancellationToken);
var applicantModel = new ApplicantModel var applicantModel = mapper.Map<ApplicantModel>(applicant);
{
Citizenship = applicant.Citizenship, var model = mapper.Map<VisaApplicationModelForAuthority>(visaApplication);
Gender = applicant.Gender, model.Applicant = applicantModel;
Name = applicant.Name,
Passport = applicant.Passport, return model;
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
};
} }
public async Task<List<VisaApplicationModelForApplicant>> GetForApplicantAsync(Guid userId, CancellationToken cancellationToken) public async Task<List<VisaApplicationModelForApplicant>> GetForApplicantAsync(Guid userId, CancellationToken cancellationToken)
{ {
//todo mapper
var applicantId = await applicants.GetApplicantIdByUserId(userId, cancellationToken); var applicantId = await applicants.GetApplicantIdByUserId(userId, cancellationToken);
var visaApplications = await applications.GetOfApplicantAsync(applicantId, cancellationToken); var visaApplications = await applications.GetOfApplicantAsync(applicantId, cancellationToken);
return visaApplications.Select(va => new VisaApplicationModelForApplicant return mapper.Map<List<VisaApplicationModelForApplicant>>(visaApplications);
{
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();
} }
public async Task HandleCreateRequestAsync(Guid userId, VisaApplicationCreateRequest request, CancellationToken cancellationToken) public async Task HandleCreateRequestAsync(Guid userId, VisaApplicationCreateRequest request, CancellationToken cancellationToken)
{ {
//TODO mapper
var applicant = await applicants.FindByUserIdAsync(userId, cancellationToken); var applicant = await applicants.FindByUserIdAsync(userId, cancellationToken);
var visaApplication = new VisaApplication var visaApplication = mapper.Map<VisaApplication>(request);
{ visaApplication.RequestDate = dateTimeProvider.Now();
ApplicantId = applicant.Id, 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
};
await applications.AddAsync(visaApplication, cancellationToken); await applications.AddAsync(visaApplication, cancellationToken);
@@ -134,11 +79,9 @@ public class VisaApplicationRequestsHandler(
var application = await applications.GetByIdAsync(applicationId, cancellationToken); var application = await applications.GetByIdAsync(applicationId, cancellationToken);
if (application.Status != ApplicationStatus.Pending) if (application.Status != ApplicationStatus.Pending)
{ {
//todo refactor exceptions
throw new ApplicationAlreadyProcessedException(); throw new ApplicationAlreadyProcessedException();
} }
//todo mapper
ApplicationStatus statusToSet = status switch ApplicationStatus statusToSet = status switch
{ {
AuthorityRequestStatuses.Approved => ApplicationStatus.Approved, AuthorityRequestStatuses.Approved => ApplicationStatus.Approved,

View File

@@ -0,0 +1,28 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains.VisaApplicationDomain;
using FluentValidation;
namespace ApplicationLayer.Services.VisaApplications.Requests.Validation
{
public class PastVisaValidator : AbstractValidator<PastVisa>
{
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");
}
}
}

View File

@@ -0,0 +1,31 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains;
using Domains.VisaApplicationDomain;
using FluentValidation;
namespace ApplicationLayer.Services.VisaApplications.Requests.Validation
{
public class PastVisitValidator : AbstractValidator<PastVisit>
{
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}");
}
}
}

View File

@@ -0,0 +1,25 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains;
using Domains.VisaApplicationDomain;
using FluentValidation;
namespace ApplicationLayer.Services.VisaApplications.Requests.Validation
{
public class PermissionToDestCountryValidator : AbstractValidator<PermissionToDestCountry?>
{
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}");
}
}
}

View File

@@ -0,0 +1,25 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains;
using Domains.VisaApplicationDomain;
using FluentValidation;
namespace ApplicationLayer.Services.VisaApplications.Requests.Validation
{
public class ReentryPermitValidator : AbstractValidator<ReentryPermit?>
{
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");
}
}
}

View File

@@ -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<VisaApplicationCreateRequest>
{
public VisaApplicationCreateRequestValidator(
IValidator<ReentryPermit?> reentryPermitValidator,
IValidator<PastVisa> pastVisaValidator,
IValidator<PermissionToDestCountry?> permissionToDestCountryValidator,
IValidator<PastVisit> 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);
}
}
}

View File

@@ -4,7 +4,7 @@ namespace ApplicationLayer.Services.VisaApplications.Requests;
/// Model of visa request from user /// Model of visa request from user
public record VisaApplicationCreateRequest( public record VisaApplicationCreateRequest(
ReentryPermit ReentryPermit, ReentryPermit? ReentryPermit,
string DestinationCountry, string DestinationCountry,
VisaCategory VisaCategory, VisaCategory VisaCategory,
bool IsForGroup, bool IsForGroup,

View File

@@ -1,4 +1,4 @@
namespace Infrastructure.Database namespace Domains
{ {
public static class ConfigurationConstraints public static class ConfigurationConstraints
{ {
@@ -13,8 +13,12 @@
public const int NameLength = 50; public const int NameLength = 50;
public const int BuildingNumberLength = 10; public const int BuildingNumberLength = 10;
public const int PassportNumberLength = 20; 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 EmailLength = 254;
public const int PasswordLength = 50; public const int PasswordLength = 50;
public const int ApplicantMinAge = 14;
public const int JobTitleLength = 50;
public const int MaxValidDays = 90;
} }
} }

View File

@@ -6,4 +6,8 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
</ItemGroup>
</Project> </Project>

View File

@@ -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<Applicant, ApplicantModel>(MemberList.Destination);
CreateMap<RegisterApplicantRequest, Applicant>(MemberList.Destination)
.ForMember(a => a.UserId, opts => opts.Ignore())
.ForMember(a => a.Name,
opts => opts.MapFrom(r => r.ApplicantName));
}
}
}

View File

@@ -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<PlaceOfWorkModel, PlaceOfWork>(MemberList.Destination)
.ForMember(p => p.Id,
opts => opts.UseDestinationValue());
}
}
}

View File

@@ -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<AuthData, User>(MemberList.Destination)
.ForMember(u => u.Role,
opts => opts.Ignore());
}
}
}

View File

@@ -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<VisaApplication, VisaApplicationModelForApplicant>(MemberList.Destination);
CreateMap<VisaApplication, VisaApplicationModelForAuthority>(MemberList.Destination)
.ForMember(model => model.Applicant,
opts => opts.Ignore());
CreateMap<VisaApplicationCreateRequest, VisaApplication>(MemberList.Destination)
.ForMember(va => va.RequestDate,
opts => opts.Ignore())
.ForMember(va => va.ApplicantId,
opts => opts.Ignore());
}
}
}

View File

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

View File

@@ -1,4 +1,5 @@
using Domains.ApplicantDomain; using Domains;
using Domains.ApplicantDomain;
using Domains.Users; using Domains.Users;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Builders;
@@ -31,5 +32,9 @@ public class ApplicantConfiguration : IEntityTypeConfiguration<Applicant>
entity.Property(a => a.CityOfBirth) entity.Property(a => a.CityOfBirth)
.IsUnicode(false) .IsUnicode(false)
.HasMaxLength(ConfigurationConstraints.CityNameLength); .HasMaxLength(ConfigurationConstraints.CityNameLength);
entity.Property(a => a.JobTitle)
.IsUnicode(false)
.HasMaxLength(ConfigurationConstraints.JobTitleLength);
} }
} }

View File

@@ -1,4 +1,5 @@
using Domains.ApplicantDomain; using Domains;
using Domains.ApplicantDomain;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Builders;

View File

@@ -18,7 +18,8 @@ public sealed class ApplicantsRepository(IGenericReader reader, IGenericWriter w
.Include(a => a.PlaceOfWork); .Include(a => a.PlaceOfWork);
} }
async Task<Applicant> IApplicantsRepository.FindByUserIdAsync(Guid userId, CancellationToken cancellationToken) /// <inheritdoc cref="IApplicantsRepository.FindByUserIdAsync"/>
public async Task<Applicant> FindByUserIdAsync(Guid userId, CancellationToken cancellationToken)
{ {
var result = await LoadDomain().SingleOrDefaultAsync(a => a.UserId == userId, cancellationToken); var result = await LoadDomain().SingleOrDefaultAsync(a => a.UserId == userId, cancellationToken);
return result ?? throw new ApplicantNotFoundByUserIdException(); 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); var result = await base.LoadDomain().SingleOrDefaultAsync(a => a.UserId == userId, cancellationToken);
return result?.Id ?? throw new ApplicantNotFoundByUserIdException(); return result?.Id ?? throw new ApplicantNotFoundByUserIdException();
} }
async Task<bool> IApplicantsRepository.IsApplicantNonResidentByUserId(Guid userId, CancellationToken cancellationToken)
{
var applicant = await FindByUserIdAsync(userId, cancellationToken);
return applicant.IsNonResident;
}
} }

View File

@@ -1,4 +1,5 @@
using Domains.Users; using Domains;
using Domains.Users;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Builders;

View File

@@ -1,4 +1,5 @@
using Domains.ApplicantDomain; using Domains;
using Domains.ApplicantDomain;
using Domains.VisaApplicationDomain; using Domains.VisaApplicationDomain;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Builders;

View File

@@ -1,4 +1,5 @@
using ApplicationLayer.InfrastructureServicesInterfaces; using System.Reflection;
using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.Applicants.NeededServices; using ApplicationLayer.Services.Applicants.NeededServices;
using ApplicationLayer.Services.AuthServices.NeededServices; using ApplicationLayer.Services.AuthServices.NeededServices;
using ApplicationLayer.Services.VisaApplications.NeededServices; using ApplicationLayer.Services.VisaApplications.NeededServices;
@@ -37,6 +38,11 @@ public static class DependencyInjection
services.AddSingleton<IDateTimeProvider, DateTimeProvider>(); services.AddSingleton<IDateTimeProvider, DateTimeProvider>();
services.AddHttpContextAccessor();
services.AddScoped<IUserIdProvider, UserIdProvider>();
services.AddAutoMapper(Assembly.GetExecutingAssembly());
return services; return services;
} }
} }

View File

@@ -1,8 +1,11 @@
using ApplicationLayer.Services.ApprovingAuthorities; using ApplicationLayer.Services.AuthServices.Common;
using ApplicationLayer.Services.AuthServices.LoginService; using ApplicationLayer.Services.AuthServices.LoginService;
using ApplicationLayer.Services.AuthServices.RegisterService; using ApplicationLayer.Services.AuthServices.RegisterService;
using ApplicationLayer.Services.AuthServices.Requests; using ApplicationLayer.Services.AuthServices.Requests;
using ApplicationLayer.Services.Users;
using ApplicationLayer.Services.Users.Requests;
using Domains.Users; using Domains.Users;
using FluentValidation;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using SchengenVisaApi.Common; using SchengenVisaApi.Common;
@@ -15,16 +18,21 @@ namespace SchengenVisaApi.Controllers
public class UsersController( public class UsersController(
IRegisterService registerService, IRegisterService registerService,
ILoginService loginService, ILoginService loginService,
IUsersService authorityService) : VisaApiControllerBase IUsersService usersService,
IValidator<RegisterApplicantRequest> registerApplicantRequestValidator,
IValidator<AuthData> authDataValidator) : ControllerBase
{ {
/// <summary> Adds applicant with user account to DB </summary> /// <summary> Adds applicant with user account to DB </summary>
[HttpPost] [HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Route("register")] [Route("register")]
public async Task<IActionResult> Register(RegisterApplicantRequest request, CancellationToken cancellationToken) public async Task<IActionResult> Register(RegisterApplicantRequest request, CancellationToken cancellationToken)
{ {
await registerService.Register(request, cancellationToken); await registerApplicantRequestValidator.ValidateAndThrowAsync(request, cancellationToken);
await registerService.RegisterApplicant(request, cancellationToken);
return Ok(); return Ok();
} }
@@ -35,10 +43,13 @@ namespace SchengenVisaApi.Controllers
[ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Route("authorities")] [Route("authorities")]
[Authorize(policy: PolicyConstants.AdminPolicy)] [Authorize(policy: PolicyConstants.AdminPolicy)]
public async Task<IActionResult> RegisterAuthority(RegisterRequest request, CancellationToken cancellationToken) public async Task<IActionResult> RegisterAuthority(RegisterRequest request, CancellationToken cancellationToken)
{ {
await authDataValidator.ValidateAndThrowAsync(request.AuthData, cancellationToken);
await registerService.RegisterAuthority(request, cancellationToken); await registerService.RegisterAuthority(request, cancellationToken);
return Ok(); return Ok();
} }
@@ -50,7 +61,7 @@ namespace SchengenVisaApi.Controllers
[Route("login")] [Route("login")]
public async Task<IActionResult> Login(string email, string password, CancellationToken cancellationToken) public async Task<IActionResult> 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); return Ok(result);
} }
@@ -64,7 +75,7 @@ namespace SchengenVisaApi.Controllers
[Authorize(policy: PolicyConstants.AdminPolicy)] [Authorize(policy: PolicyConstants.AdminPolicy)]
public async Task<IActionResult> GetAuthorityAccounts(CancellationToken cancellationToken) public async Task<IActionResult> GetAuthorityAccounts(CancellationToken cancellationToken)
{ {
var result = await authorityService.GetAuthoritiesAccountsAsync(cancellationToken); var result = await usersService.GetAuthoritiesAccountsAsync(cancellationToken);
return Ok(result); return Ok(result);
} }
@@ -75,12 +86,14 @@ namespace SchengenVisaApi.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Route("authorities/{authorityAccountId:guid}")] [Route("authorities/{authorityAccountId:guid}")]
[Authorize(policy: PolicyConstants.AdminPolicy)] [Authorize(policy: PolicyConstants.AdminPolicy)]
//todo replace args with ChangeAuthorityAuthDataRequest or something public async Task<IActionResult> ChangeAuthorityAuthData(Guid authorityAccountId, AuthData authData, CancellationToken cancellationToken)
public async Task<IActionResult> ChangeAuthorityAuthData(Guid authorityAccountId, RegisterRequest authData, CancellationToken cancellationToken)
{ {
await authorityService.ChangeAccountAuthDataAsync(authorityAccountId, authData, cancellationToken); await authDataValidator.ValidateAndThrowAsync(authData, cancellationToken);
await usersService.ChangeAccountAuthDataAsync(new ChangeUserAuthDataRequest(authorityAccountId, authData), cancellationToken);
return Ok(); return Ok();
} }
@@ -95,7 +108,7 @@ namespace SchengenVisaApi.Controllers
[Authorize(policy: PolicyConstants.AdminPolicy)] [Authorize(policy: PolicyConstants.AdminPolicy)]
public async Task<IActionResult> RemoveAuthorityAccount(Guid authorityAccountId, CancellationToken cancellationToken) public async Task<IActionResult> RemoveAuthorityAccount(Guid authorityAccountId, CancellationToken cancellationToken)
{ {
await authorityService.RemoveUserAccount(authorityAccountId, cancellationToken); await usersService.RemoveUserAccount(authorityAccountId, cancellationToken);
return Ok(); return Ok();
} }
} }

View File

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

View File

@@ -1,6 +1,8 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.VisaApplications.Handlers; using ApplicationLayer.Services.VisaApplications.Handlers;
using ApplicationLayer.Services.VisaApplications.Models; using ApplicationLayer.Services.VisaApplications.Models;
using ApplicationLayer.Services.VisaApplications.Requests; using ApplicationLayer.Services.VisaApplications.Requests;
using FluentValidation;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using SchengenVisaApi.Common; using SchengenVisaApi.Common;
@@ -10,7 +12,10 @@ namespace SchengenVisaApi.Controllers;
/// <summary> Controller for <see cref="Domains.VisaApplicationDomain"/> </summary> /// <summary> Controller for <see cref="Domains.VisaApplicationDomain"/> </summary>
[ApiController] [ApiController]
[Route("visaApplications")] [Route("visaApplications")]
public class VisaApplicationController(IVisaApplicationRequestsHandler visaApplicationRequestsHandler) : VisaApiControllerBase public class VisaApplicationController(
IVisaApplicationRequestsHandler visaApplicationRequestsHandler,
IUserIdProvider userIdProvider,
IValidator<VisaApplicationCreateRequest> visaApplicationCreateRequestValidator) : ControllerBase
{ {
/// <summary> Returns all applications from DB </summary> /// <summary> Returns all applications from DB </summary>
/// <remarks> Accessible only for approving authorities </remarks> /// <remarks> Accessible only for approving authorities </remarks>
@@ -36,7 +41,7 @@ public class VisaApplicationController(IVisaApplicationRequestsHandler visaAppli
[Route("OfApplicant")] [Route("OfApplicant")]
public async Task<IActionResult> GetForApplicant(CancellationToken cancellationToken) public async Task<IActionResult> GetForApplicant(CancellationToken cancellationToken)
{ {
var userId = GetUserId(); var userId = userIdProvider.GetUserId();
var result = await visaApplicationRequestsHandler.GetForApplicantAsync(userId, cancellationToken); var result = await visaApplicationRequestsHandler.GetForApplicantAsync(userId, cancellationToken);
return Ok(result); return Ok(result);
} }
@@ -48,10 +53,13 @@ public class VisaApplicationController(IVisaApplicationRequestsHandler visaAppli
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Authorize(policy: PolicyConstants.ApplicantPolicy)] [Authorize(policy: PolicyConstants.ApplicantPolicy)]
public async Task<IActionResult> Create(VisaApplicationCreateRequest request, CancellationToken cancellationToken) public async Task<IActionResult> Create(VisaApplicationCreateRequest request, CancellationToken cancellationToken)
{ {
var userId = GetUserId(); await visaApplicationCreateRequestValidator.ValidateAndThrowAsync(request, cancellationToken);
var userId = userIdProvider.GetUserId();
await visaApplicationRequestsHandler.HandleCreateRequestAsync(userId, request, cancellationToken); await visaApplicationRequestsHandler.HandleCreateRequestAsync(userId, request, cancellationToken);
return Ok(); return Ok();
} }
@@ -67,7 +75,7 @@ public class VisaApplicationController(IVisaApplicationRequestsHandler visaAppli
[Route("{applicationId:guid}")] [Route("{applicationId:guid}")]
public async Task<IActionResult> CloseApplication(Guid applicationId, CancellationToken cancellationToken) public async Task<IActionResult> CloseApplication(Guid applicationId, CancellationToken cancellationToken)
{ {
var userId = GetUserId(); var userId = userIdProvider.GetUserId();
await visaApplicationRequestsHandler.HandleCloseRequestAsync(userId, applicationId, cancellationToken); await visaApplicationRequestsHandler.HandleCloseRequestAsync(userId, applicationId, cancellationToken);
return Ok(); return Ok();
} }

View File

@@ -2,6 +2,7 @@
using ApplicationLayer.Services.AuthServices.LoginService.Exceptions; using ApplicationLayer.Services.AuthServices.LoginService.Exceptions;
using ApplicationLayer.Services.GeneralExceptions; using ApplicationLayer.Services.GeneralExceptions;
using ApplicationLayer.Services.VisaApplications.Exceptions; using ApplicationLayer.Services.VisaApplications.Exceptions;
using FluentValidation;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
@@ -16,43 +17,52 @@ namespace SchengenVisaApi.ExceptionFilters
var exception = context.Exception; var exception = context.Exception;
var problemDetails = new ProblemDetails(); var problemDetails = new ProblemDetails();
if (exception is ApiException) switch (exception)
{ {
problemDetails.Detail = exception.Message; case ValidationException validationException:
switch (exception) problemDetails.Extensions.Add("Errors", validationException.Errors.Select(e => e.ErrorMessage));
{ problemDetails.Detail = "Validation errors occured";
case EntityNotFoundException: problemDetails.Status = StatusCodes.Status400BadRequest;
problemDetails.Status = StatusCodes.Status404NotFound; problemDetails.Title = "Bad request";
problemDetails.Title = "Requested entity not found"; problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1";
problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4"; break;
break; case ApiException:
case IncorrectLoginDataException: problemDetails.Detail = exception.Message;
problemDetails.Status = StatusCodes.Status403Forbidden; switch (exception)
problemDetails.Title = "Auth failed"; {
problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.3"; case EntityNotFoundException:
break; problemDetails.Status = StatusCodes.Status404NotFound;
case AlreadyExistsException: problemDetails.Title = "Requested entity not found";
problemDetails.Status = StatusCodes.Status409Conflict; problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4";
problemDetails.Title = "Already exists"; break;
problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.8"; case IncorrectLoginDataException:
break; problemDetails.Status = StatusCodes.Status403Forbidden;
case ApplicationAlreadyProcessedException: problemDetails.Title = "Auth failed";
problemDetails.Status = StatusCodes.Status409Conflict; problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.3";
problemDetails.Title = "Already processed"; break;
problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.8"; case AlreadyExistsException:
break; problemDetails.Status = StatusCodes.Status409Conflict;
default: problemDetails.Title = "Already exists";
problemDetails.Status = StatusCodes.Status400BadRequest; problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.8";
problemDetails.Title = "Bad request"; break;
problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1"; case ApplicationAlreadyProcessedException:
break; problemDetails.Status = StatusCodes.Status409Conflict;
} problemDetails.Title = "Already processed";
} problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.8";
else break;
{ default:
problemDetails.Status = StatusCodes.Status500InternalServerError; problemDetails.Status = StatusCodes.Status400BadRequest;
problemDetails.Title = "An unhandled error occured"; problemDetails.Title = "Bad request";
problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1"; 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); await Results.Problem(problemDetails).ExecuteAsync(context.HttpContext);