Merge pull request #13 from prtsie/12-client

12 client
This commit is contained in:
prtsie
2024-09-16 15:16:28 +03:00
committed by GitHub
138 changed files with 6822 additions and 604 deletions

View File

@@ -0,0 +1,13 @@
using System.Text.RegularExpressions;
namespace ApplicationLayer
{
public static class Constants
{
public readonly static Regex EnglishWordRegex = new("^[a-zA-Z]*$");
public readonly static Regex EnglishPhraseRegex = new(@"^[a-zA-Z№0-9?><;,{}[\]\-_+=!@#$%\^&*|']*$");
public readonly static Regex PhoneNumRegex = new(@"^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$");
}
}

View File

@@ -1,6 +1,6 @@
using Domains;
namespace ApplicationLayer.Services.GeneralExceptions;
namespace ApplicationLayer.GeneralExceptions;
/// Exception to throw when entity not found
/// <param name="id">Identifier of entity</param>

View File

@@ -1,6 +1,4 @@
using ApplicationLayer.GeneralExceptions;
namespace ApplicationLayer.Services.GeneralExceptions;
namespace ApplicationLayer.GeneralExceptions;
/// Exception to throw when entity not found
public class EntityNotFoundException(string message) : ApiException(message);

View File

@@ -0,0 +1,11 @@
using ApplicationLayer.Services.AuthServices.Common;
using Domains.Users;
namespace ApplicationLayer.InfrastructureServicesInterfaces;
/// Generates jwt-tokens
public interface ITokenGenerator
{
/// returns jwt-token for specific user
AuthToken CreateToken(User user);
}

View File

@@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.Applicants.Models;
public class AddressModel
{
/// Country part of address
[Required]
[MaxLength(ConfigurationConstraints.CountryNameLength)]
public string Country { get; set; } = null!;
/// City part of address
[Required]
[MaxLength(ConfigurationConstraints.CityNameLength)]
public string City { get; set; } = null!;
/// Street part of address
[Required]
[MaxLength(ConfigurationConstraints.StreetNameLength)]
public string Street { get; set; } = null!;
/// Building part of address
[Required]
[MaxLength(ConfigurationConstraints.BuildingNumberLength)]
public string Building { get; set; } = null!;
}

View File

@@ -1,49 +1,64 @@
using Domains.ApplicantDomain;
using System.ComponentModel.DataAnnotations;
using Domains.ApplicantDomain;
namespace ApplicationLayer.Services.Applicants.Models;
/// Model of <see cref="Applicant"/>
/// Model of <see cref="Applicant" />
public class ApplicantModel
{
/// <inheritdoc cref="Applicant.Name"/>
public Name Name { get; set; } = null!;
/// <inheritdoc cref="Applicant.Name" />
[Required]
public NameModel Name { get; set; } = null!;
/// <inheritdoc cref="Applicant.Passport"/>
public Passport Passport { get; set; } = null!;
/// <inheritdoc cref="Applicant.Passport" />
[Required]
public PassportModel Passport { get; set; } = null!;
/// <inheritdoc cref="Applicant.BirthDate"/>
/// <inheritdoc cref="Applicant.BirthDate" />
[Required]
public DateTime BirthDate { get; set; }
/// <inheritdoc cref="Applicant.CountryOfBirth"/>
/// <inheritdoc cref="Applicant.CountryOfBirth" />
[Required]
public string CountryOfBirth { get; set; } = null!;
/// <inheritdoc cref="Applicant.CityOfBirth"/>
/// <inheritdoc cref="Applicant.CityOfBirth" />
[Required]
public string CityOfBirth { get; set; } = null!;
/// <inheritdoc cref="Applicant.Citizenship"/>
/// <inheritdoc cref="Applicant.Citizenship" />
[Required]
public string Citizenship { get; set; } = null!;
/// <inheritdoc cref="Applicant.CitizenshipByBirth"/>
/// <inheritdoc cref="Applicant.CitizenshipByBirth" />
[Required]
public string CitizenshipByBirth { get; set; } = null!;
/// <inheritdoc cref="Applicant.Gender"/>
/// <inheritdoc cref="Applicant.Gender" />
[Required]
public Gender Gender { get; set; }
/// <inheritdoc cref="Applicant.MaritalStatus"/>
/// <inheritdoc cref="Applicant.MaritalStatus" />
[Required]
public MaritalStatus MaritalStatus { get; set; }
/// <inheritdoc cref="Applicant.FatherName"/>
public Name FatherName { get; set; } = null!;
/// <inheritdoc cref="Applicant.FatherName" />
[Required]
public NameModel FatherName { get; set; } = null!;
/// <inheritdoc cref="Applicant.MotherName"/>
public Name MotherName { get; set; } = null!;
/// <inheritdoc cref="Applicant.MotherName" />
[Required]
public NameModel MotherName { get; set; } = null!;
/// <inheritdoc cref="Applicant.JobTitle"/>
/// <inheritdoc cref="Applicant.JobTitle" />
[Required]
public string JobTitle { get; set; } = null!;
/// <inheritdoc cref="Applicant.PlaceOfWork"/>
public PlaceOfWork PlaceOfWork { get; set; } = null!;
/// <inheritdoc cref="Applicant.PlaceOfWork" />
[Required]
public PlaceOfWorkModel PlaceOfWork { get; set; } = null!;
/// <inheritdoc cref="Applicant.IsNonResident"/>
/// <inheritdoc cref="Applicant.IsNonResident" />
[Required]
public bool IsNonResident { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.Applicants.Models;
/// Model of name for presentation layer
public class NameModel
{
[Required]
[MaxLength(ConfigurationConstraints.NameLength)]
public string FirstName { get; set; } = null!;
[Required]
[MaxLength(ConfigurationConstraints.NameLength)]
public string Surname { get; set; } = null!;
[MaxLength(ConfigurationConstraints.NameLength)]
public string? Patronymic { get; set; }
}

View File

@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.Applicants.Models;
/// Model of passport fpr presentation layer
public class PassportModel
{
/// Number of passport
[Required]
[MaxLength(ConfigurationConstraints.PassportNumberLength)]
public string Number { get; set; } = null!;
/// Issuing authority of passport
[Required]
[MaxLength(ConfigurationConstraints.IssuerNameLength)]
public string Issuer { get; set; } = null!;
/// Date of issue
[Required]
public DateTime IssueDate { get; set; }
/// Date when the passport expires
[Required]
public DateTime ExpirationDate { get; set; }
}

View File

@@ -1,15 +1,22 @@
using Domains.ApplicantDomain;
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.Applicants.Models;
public class PlaceOfWorkModel
{
/// Name of hirer
[Required]
[MaxLength(ConfigurationConstraints.PlaceOfWorkNameLength)]
public string Name { get; set; } = null!;
/// Address of hirer
public Address Address { get; set; } = null!;
[Required]
public AddressModel Address { get; set; } = null!;
/// Phone number of hirer
[Required]
[MaxLength(ConfigurationConstraints.PhoneNumberLength)]
[MinLength(ConfigurationConstraints.PhoneNumberMinLength)]
public string PhoneNum { get; set; } = null!;
}

View File

@@ -0,0 +1,32 @@
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.Applicants.Models.Validation;
public class NameModelValidator : AbstractValidator<NameModel>
{
public NameModelValidator()
{
RuleFor(m => m.FirstName)
.NotEmpty()
.WithMessage("First Name can not be empty")
.Matches(Constants.EnglishWordRegex)
.WithMessage("First name must be in english characters")
.MaximumLength(ConfigurationConstraints.NameLength)
.WithMessage($"First Name length must be less than {ConfigurationConstraints.NameLength}");
RuleFor(m => m.Surname)
.NotEmpty()
.WithMessage("Surname can not be empty")
.Matches(Constants.EnglishWordRegex)
.WithMessage("Surname must be in english characters")
.MaximumLength(ConfigurationConstraints.NameLength)
.WithMessage($"Surname length must be less than {ConfigurationConstraints.NameLength}");
RuleFor(m => m.Patronymic)
.Matches(Constants.EnglishWordRegex)
.WithMessage("Patronymic must be in english characters")
.MaximumLength(ConfigurationConstraints.NameLength)
.WithMessage($"Patronymic length must be less than {ConfigurationConstraints.NameLength}");
}
}

View File

@@ -0,0 +1,39 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.Applicants.Models.Validation;
public class PassportModelValidator : AbstractValidator<PassportModel>
{
public PassportModelValidator(IDateTimeProvider dateTimeProvider)
{
RuleFor(r => r.Issuer)
.NotEmpty()
.WithMessage("Passport issuer can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Passport issuer field can contain only english letters, digits and special symbols")
.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")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Passport number field can contain only english letters, digits and special symbols")
.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,66 @@
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.Applicants.Models.Validation;
public class PlaceOfWorkModelValidator : AbstractValidator<PlaceOfWorkModel>
{
public PlaceOfWorkModelValidator()
{
RuleFor(p => p.Name)
.NotEmpty()
.WithMessage("Place of work name can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Place of work name field can contain only english letters, digits and special symbols")
.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")
.Matches(Constants.PhoneNumRegex)
.WithMessage("Place of work phone number field must be valid")
.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)
.NotEmpty()
.WithMessage("Place of work address can not be empty");
RuleFor(p => p.Address.Country)
.NotEmpty()
.WithMessage("Country name of place of work can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Place of work Country field can contain only english letters, digits and special symbols")
.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")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Place of work City field can contain only english letters, digits and special symbols")
.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")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Place of work Street field can contain only english letters, digits and special symbols")
.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")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Place of work building field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.CountryNameLength)
.WithMessage($"Building of place of work length must be less than {ConfigurationConstraints.BuildingNumberLength}");
}
}

View File

@@ -1,3 +1,15 @@
namespace ApplicationLayer.Services.AuthServices.Common;
using System.ComponentModel.DataAnnotations;
using Domains;
public record AuthData(string Email, string Password);
namespace ApplicationLayer.Services.AuthServices.Common;
public class AuthData
{
[Required]
[MaxLength(ConfigurationConstraints.EmailLength)]
public string Email { get; set; } = null!;
[Required]
[MaxLength(ConfigurationConstraints.PasswordLength)]
public string Password { get; set; } = null!;
}

View File

@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace ApplicationLayer.Services.AuthServices.Common
{
public class AuthToken
{
[Required] public string Token { get; set; } = null!;
}
}

View File

@@ -1,26 +1,29 @@
using ApplicationLayer.Services.AuthServices.LoginService.Exceptions;
using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.AuthServices.Common;
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<string> ILoginService.LoginAsync(string email, string password, CancellationToken cancellationToken)
async Task<AuthToken> ILoginService.LoginAsync(LoginRequest request, CancellationToken cancellationToken)
{
if (email == "admin@mail.ru" && password == "admin")
{
var admin = new User { Role = Role.Admin };
if (request.AuthData is { Email: "admin@mail.ru", Password: "admin" })
{
var admin = new User { Role = Role.Admin };
return tokenGenerator.CreateToken(admin);
}
var user = await users.FindByEmailAsync(email, cancellationToken);
if (user is null || user.Password != password)
{
throw new IncorrectLoginDataException();
}
return tokenGenerator.CreateToken(user);
return tokenGenerator.CreateToken(admin);
}
}
var user = await users.FindByEmailAsync(request.AuthData.Email, cancellationToken);
if (user is null || user.Password != request.AuthData.Password)
{
throw new IncorrectLoginDataException();
}
return tokenGenerator.CreateToken(user);
}
}

View File

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

View File

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

View File

@@ -1,10 +0,0 @@
using Domains.Users;
namespace ApplicationLayer.Services.AuthServices.NeededServices;
/// Generates jwt-tokens
public interface ITokenGenerator
{
/// returns jwt-token for specific user
string CreateToken(User user);
}

View File

@@ -17,7 +17,7 @@ public class RegisterService(
{
async Task IRegisterService.RegisterApplicant(RegisterApplicantRequest request, CancellationToken cancellationToken)
{
var user = mapper.Map<User>(request.AuthData);
var user = mapper.Map<User>(request.RegisterRequest.AuthData);
user.Role = Role.Applicant;
var applicant = mapper.Map<Applicant>(request);

View File

@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
using ApplicationLayer.Services.AuthServices.Common;
namespace ApplicationLayer.Services.AuthServices.Requests;
public class LoginRequest
{
[Required] public AuthData AuthData { get; set; } = null!;
}

View File

@@ -1,22 +1,49 @@
using ApplicationLayer.Services.Applicants.Models;
using ApplicationLayer.Services.AuthServices.Common;
using System.ComponentModel.DataAnnotations;
using ApplicationLayer.Services.Applicants.Models;
using Domains;
using Domains.ApplicantDomain;
namespace ApplicationLayer.Services.AuthServices.Requests;
public record RegisterApplicantRequest(
AuthData AuthData,
Name ApplicantName,
Passport Passport,
DateTime BirthDate,
string CityOfBirth,
string CountryOfBirth,
string Citizenship,
string CitizenshipByBirth,
Gender Gender,
MaritalStatus MaritalStatus,
Name FatherName,
Name MotherName,
string JobTitle,
PlaceOfWorkModel PlaceOfWork,
bool IsNonResident) : RegisterRequest(AuthData);
public record RegisterApplicantRequest
{
[Required] public RegisterRequest RegisterRequest { get; set; } = null!;
[Required] public NameModel ApplicantName { get; set; } = null!;
[Required] public PassportModel Passport { get; set; } = null!;
[Required] public DateTime BirthDate { get; set; }
[Required]
[MaxLength(ConfigurationConstraints.CityNameLength)]
public string CityOfBirth { get; set; } = null!;
[Required]
[MaxLength(ConfigurationConstraints.CountryNameLength)]
public string CountryOfBirth { get; set; } = null!;
[Required]
[MaxLength(ConfigurationConstraints.CitizenshipLength)]
public string Citizenship { get; set; } = null!;
[Required]
[MaxLength(ConfigurationConstraints.CitizenshipLength)]
public string CitizenshipByBirth { get; set; } = null!;
[Required] public Gender Gender { get; set; }
[Required] public MaritalStatus MaritalStatus { get; set; }
[Required] public NameModel FatherName { get; set; } = null!;
[Required] public NameModel MotherName { get; set; } = null!;
[Required]
[MaxLength(ConfigurationConstraints.JobTitleLength)]
public string JobTitle { get; set; } = null!;
[Required] public PlaceOfWorkModel PlaceOfWork { get; set; } = null!;
[Required] public bool IsNonResident { get; set; }
}

View File

@@ -1,5 +1,9 @@
using ApplicationLayer.Services.AuthServices.Common;
using System.ComponentModel.DataAnnotations;
using ApplicationLayer.Services.AuthServices.Common;
namespace ApplicationLayer.Services.AuthServices.Requests;
public record RegisterRequest(AuthData AuthData);
public class RegisterRequest
{
[Required] public AuthData AuthData { get; set; } = null!;
}

View File

@@ -1,5 +1,4 @@
using ApplicationLayer.Services.AuthServices.Common;
using ApplicationLayer.Services.AuthServices.NeededServices;
using Domains;
using FluentValidation;
@@ -7,25 +6,22 @@ namespace ApplicationLayer.Services.AuthServices.Requests.Validation;
public class AuthDataValidator : AbstractValidator<AuthData>
{
public AuthDataValidator(IUsersRepository users)
public AuthDataValidator()
{
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.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}");
RuleFor(d => d.Password)
.NotEmpty()
.WithMessage("Password can not be empty")
.MaximumLength(ConfigurationConstraints.PasswordLength)
.WithMessage($"Password length must be less than {ConfigurationConstraints.PasswordLength}");
}
}
RuleFor(d => d.Password)
.NotEmpty()
.WithMessage("Password can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Password can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.PasswordLength)
.WithMessage($"Password length must be less than {ConfigurationConstraints.PasswordLength}");
}
}

View File

@@ -1,27 +0,0 @@
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

@@ -1,36 +0,0 @@
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

@@ -1,49 +0,0 @@
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

@@ -1,8 +1,6 @@
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;
@@ -11,67 +9,85 @@ public class RegisterApplicantRequestValidator : AbstractValidator<RegisterAppli
{
public RegisterApplicantRequestValidator(
IDateTimeProvider dateTimeProvider,
IValidator<Name> nameValidator,
IValidator<AuthData> authDataValidator,
IValidator<Passport> passportValidator,
IValidator<NameModel> nameValidator,
IValidator<RegisterRequest> registerRequestValidator,
IValidator<PassportModel> passportValidator,
IValidator<PlaceOfWorkModel> placeOfWorkModelValidator)
{
RuleFor(r => r.AuthData)
.SetValidator(authDataValidator);
RuleFor(r => r.RegisterRequest)
.NotEmpty()
.SetValidator(registerRequestValidator);
RuleFor(r => r.ApplicantName)
.SetValidator(nameValidator);
RuleFor(r => r.ApplicantName)
.NotEmpty()
.SetValidator(nameValidator);
RuleFor(r => r.FatherName)
.SetValidator(nameValidator);
RuleFor(r => r.FatherName)
.NotEmpty()
.SetValidator(nameValidator);
RuleFor(r => r.MotherName)
.SetValidator(nameValidator);
RuleFor(r => r.MotherName)
.NotEmpty()
.SetValidator(nameValidator);
RuleFor(r => r.Passport)
.SetValidator(passportValidator);
RuleFor(r => r.Passport)
.NotEmpty()
.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.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.CountryOfBirth)
.NotEmpty()
.WithMessage("Country of birth can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Country of birth field can contain only english letters, digits and special symbols")
.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.CityOfBirth)
.NotEmpty()
.WithMessage("City of birth can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("City of birth field can contain only english letters, digits and special symbols")
.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.Citizenship)
.NotEmpty()
.WithMessage("Citizenship can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Citizenship field can contain only english letters, digits and special symbols")
.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.CitizenshipByBirth)
.NotEmpty()
.WithMessage("Citizenship by birth can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Citizenship by birth field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.CitizenshipLength)
.WithMessage($"Citizenship by birth length must be less than {ConfigurationConstraints.CitizenshipLength}");
RuleFor(r => r.Gender).IsInEnum();
RuleFor(r => r.Gender)
.IsInEnum();
RuleFor(r => r.MaritalStatus).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.JobTitle)
.NotEmpty()
.WithMessage("Title of job can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Title of job field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.JobTitleLength)
.WithMessage($"Title of job length must be less than {ConfigurationConstraints.JobTitleLength}");
RuleFor(r => r.PlaceOfWork)
.SetValidator(placeOfWorkModelValidator);
}
}
RuleFor(r => r.PlaceOfWork)
.NotEmpty()
.SetValidator(placeOfWorkModelValidator);
}
}

View File

@@ -0,0 +1,17 @@
using ApplicationLayer.Services.AuthServices.Common;
using ApplicationLayer.Services.AuthServices.NeededServices;
using FluentValidation;
namespace ApplicationLayer.Services.AuthServices.Requests.Validation;
public class RegisterRequestValidator : AbstractValidator<RegisterRequest>
{
public RegisterRequestValidator(IUsersRepository users, IValidator<AuthData> authDataValidator)
{
RuleFor(r => r.AuthData)
.NotEmpty()
.SetValidator(authDataValidator)
.MustAsync(async (authData, ct) => await users.FindByEmailAsync(authData.Email, ct) is null)
.WithMessage("Email already exists");
}
}

View File

@@ -0,0 +1,6 @@
using ApplicationLayer.GeneralExceptions;
using Domains.Users;
namespace ApplicationLayer.Services.Users.Exceptions;
public class WrongRoleException(Guid userId) : EntityNotFoundByIdException<User>(userId);

View File

@@ -1,5 +1,6 @@
using ApplicationLayer.Services.Users.Requests;
using Domains.Users;
using ApplicationLayer.Services.Applicants.Models;
using ApplicationLayer.Services.Users.Models;
using ApplicationLayer.Services.Users.Requests;
namespace ApplicationLayer.Services.Users;
@@ -8,15 +9,19 @@ public interface IUsersService
{
/// Returns all user accounts with role of approving authority
/// <param name="cancellationToken">Cancellation token</param>
Task<List<User>> GetAuthoritiesAccountsAsync(CancellationToken cancellationToken);
Task<List<UserModel>> GetAuthoritiesAccountsAsync(CancellationToken cancellationToken);
/// Changes authentication data for an account
/// Changes authentication data for an authority account
/// <param name="request"> Request object with identifier of user and new authentication data</param>
/// <param name="cancellationToken">Cancellation token</param>
Task ChangeAccountAuthDataAsync(ChangeUserAuthDataRequest request, CancellationToken cancellationToken);
Task ChangeAuthorityAuthDataAsync(ChangeUserAuthDataRequest request, CancellationToken cancellationToken);
/// Removes user account
/// Removes account of authority
/// <param name="userId">Identifier of account</param>
/// <param name="cancellationToken">Cancellation token</param>
Task RemoveUserAccount(Guid userId, CancellationToken cancellationToken);
}
Task RemoveAuthorityAccount(Guid userId, CancellationToken cancellationToken);
/// Get applicant that made request
/// <param name="cancellationToken">cancellation token</param>
Task<ApplicantModel> GetAuthenticatedApplicant(CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.Users.Models
{
/// Auth data with nullable password for making change auth data requests
public class ChangeAuthData
{
[Required]
[MaxLength(ConfigurationConstraints.EmailLength)]
public string Email { get; set; } = null!;
[MaxLength(ConfigurationConstraints.PasswordLength)]
public string? Password { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.Users.Models
{
public class UserModel
{
/// Unique Identifier of user
[Required]
public Guid Id { get; private set; } = Guid.NewGuid();
[Required]
[MaxLength(ConfigurationConstraints.EmailLength)]
public string Email { get; set; } = null!;
}
}

View File

@@ -1,5 +1,11 @@
using ApplicationLayer.Services.AuthServices.Common;
using System.ComponentModel.DataAnnotations;
using ApplicationLayer.Services.Users.Models;
namespace ApplicationLayer.Services.Users.Requests;
public record ChangeUserAuthDataRequest(Guid UserId, AuthData NewAuthData);
public class ChangeUserAuthDataRequest(Guid userId, ChangeAuthData newAuthData)
{
[Required] public Guid UserId { get; set; } = userId;
[Required] public ChangeAuthData NewAuthData { get; set; } = newAuthData;
}

View File

@@ -0,0 +1,21 @@
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.Users.Requests.Validation
{
public class ChangeUserAuthDataRequestValidator : AbstractValidator<ChangeUserAuthDataRequest>
{
public ChangeUserAuthDataRequestValidator()
{
RuleFor(r => r.NewAuthData)
.NotEmpty();
RuleFor(r => r.NewAuthData.Email)
.NotEmpty()
.EmailAddress()
.WithMessage("Email should be valid")
.MaximumLength(ConfigurationConstraints.EmailLength)
.WithMessage($"Email address length must be less than {ConfigurationConstraints.EmailLength}");
}
}
}

View File

@@ -1,33 +1,85 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.Applicants.Models;
using ApplicationLayer.Services.Applicants.NeededServices;
using ApplicationLayer.Services.AuthServices.NeededServices;
using ApplicationLayer.Services.Users.Exceptions;
using ApplicationLayer.Services.Users.Models;
using ApplicationLayer.Services.Users.Requests;
using AutoMapper;
using Domains.Users;
namespace ApplicationLayer.Services.Users;
public class UsersService(IUsersRepository users, IUnitOfWork unitOfWork) : IUsersService
public class UsersService(
IMapper mapper,
IUserIdProvider userIdProvider,
IUsersRepository users,
IApplicantsRepository applicants,
IUnitOfWork unitOfWork) : IUsersService
{
async Task<List<User>> IUsersService.GetAuthoritiesAccountsAsync(CancellationToken cancellationToken)
async Task<List<UserModel>> IUsersService.GetAuthoritiesAccountsAsync(CancellationToken cancellationToken)
{
return await users.GetAllOfRoleAsync(Role.ApprovingAuthority, cancellationToken);
}
var userList = await users.GetAllOfRoleAsync(Role.ApprovingAuthority, cancellationToken);
return mapper.Map<List<UserModel>>(userList);
}
async Task IUsersService.ChangeAccountAuthDataAsync(ChangeUserAuthDataRequest request, CancellationToken cancellationToken)
async Task IUsersService.ChangeAuthorityAuthDataAsync(ChangeUserAuthDataRequest request, CancellationToken cancellationToken)
{
var user = await users.GetByIdAsync(request.UserId, cancellationToken);
var user = await users.GetByIdAsync(request.UserId, cancellationToken);
user.Email = request.NewAuthData.Email;
user.Password = request.NewAuthData.Password;
await users.UpdateAsync(user, cancellationToken);
ValidateRole(user, Role.ApprovingAuthority);
await unitOfWork.SaveAsync(cancellationToken);
}
await ChangeAccountAuthDataAsync(user, request.NewAuthData, cancellationToken);
}
async Task IUsersService.RemoveUserAccount(Guid userId, CancellationToken cancellationToken)
async Task IUsersService.RemoveAuthorityAccount(Guid userId, CancellationToken cancellationToken)
{
var user = await users.GetByIdAsync(userId, cancellationToken);
users.Remove(user);
var user = await users.GetByIdAsync(userId, cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);
ValidateRole(user, Role.ApprovingAuthority);
await RemoveUserAccount(user, cancellationToken);
}
async Task<ApplicantModel> IUsersService.GetAuthenticatedApplicant(CancellationToken cancellationToken)
{
var applicant = await applicants.FindByUserIdAsync(userIdProvider.GetUserId(), cancellationToken);
return mapper.Map<ApplicantModel>(applicant);
}
/// Updates user account auth data
/// <param name="user">User to remove</param>
/// <param name="authData">New auth data</param>
/// <param name="cancellationToken">Cancellation token</param>
private async Task ChangeAccountAuthDataAsync(User user, ChangeAuthData authData, CancellationToken cancellationToken)
{
user.Email = authData.Email;
user.Password = authData.Password ?? user.Password;
await users.UpdateAsync(user, cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);
}
/// Removes user account from DB
/// <param name="user">User to remove</param>
/// <param name="cancellationToken">Cancellation token</param>
private async Task RemoveUserAccount(User user, CancellationToken cancellationToken)
{
users.Remove(user);
await unitOfWork.SaveAsync(cancellationToken);
}
/// Checks if role of user equals expected
/// <param name="user">User to check</param>
/// <param name="expectedRole">Expected role</param>
/// <exception cref="WrongRoleException">Role is not expected</exception>
private static void ValidateRole(User user, Role expectedRole)
{
if (user.Role != expectedRole)
{
throw new WrongRoleException(user.Id);
}
}
}
}

View File

@@ -6,10 +6,16 @@ namespace ApplicationLayer.Services.VisaApplications.Handlers;
public interface IVisaApplicationRequestsHandler
{
/// Returns all applications for approving authorities
Task<List<VisaApplicationModelForAuthority>> GetPendingAsync(CancellationToken cancellationToken);
Task<List<VisaApplicationPreview>> GetPendingAsync(CancellationToken cancellationToken);
/// Returns all applications of one applicant
Task<List<VisaApplicationModelForApplicant>> GetForApplicantAsync(CancellationToken cancellationToken);
Task<List<VisaApplicationPreview>> GetForApplicantAsync(CancellationToken cancellationToken);
/// Returns one application with specific id
Task<VisaApplicationModel> GetApplicationForApplicantAsync(Guid id, CancellationToken cancellationToken);
/// Returns one application with specific id
Task<VisaApplicationModel> GetApplicationForAuthorityAsync(Guid id, CancellationToken cancellationToken);
/// Creates application for applicant with specific user identifier
Task HandleCreateRequestAsync(VisaApplicationCreateRequest request, CancellationToken cancellationToken);

View File

@@ -19,36 +19,41 @@ public class VisaApplicationRequestsHandler(
IDateTimeProvider dateTimeProvider,
IUserIdProvider userIdProvider) : IVisaApplicationRequestsHandler
{
async Task<List<VisaApplicationModelForAuthority>> IVisaApplicationRequestsHandler.GetPendingAsync(CancellationToken cancellationToken)
async Task<List<VisaApplicationPreview>> IVisaApplicationRequestsHandler.GetPendingAsync(CancellationToken cancellationToken)
{
var applicationsList = await applications.GetPendingApplicationsAsync(cancellationToken);
var applicationModels = applicationsList
.Select(a => MapVisaApplicationToModelForAuthorities(a, cancellationToken).Result)
.ToList();
var applicationModels = mapper.Map<List<VisaApplicationPreview>>(applicationsList);
return applicationModels;
}
private async Task<VisaApplicationModelForAuthority> MapVisaApplicationToModelForAuthorities(VisaApplication visaApplication,
CancellationToken cancellationToken)
{
var applicant = await applicants.GetByIdAsync(visaApplication.ApplicantId, cancellationToken);
var applicantModel = mapper.Map<ApplicantModel>(applicant);
var model = mapper.Map<VisaApplicationModelForAuthority>(visaApplication);
model.Applicant = applicantModel;
return model;
}
public async Task<List<VisaApplicationModelForApplicant>> GetForApplicantAsync(CancellationToken cancellationToken)
async Task<List<VisaApplicationPreview>> IVisaApplicationRequestsHandler.GetForApplicantAsync(CancellationToken cancellationToken)
{
var applicantId = await applicants.GetApplicantIdByUserId(userIdProvider.GetUserId(), cancellationToken);
var visaApplications = await applications.GetOfApplicantAsync(applicantId, cancellationToken);
return mapper.Map<List<VisaApplicationModelForApplicant>>(visaApplications);
return mapper.Map<List<VisaApplicationPreview>>(visaApplications);
}
public async Task HandleCreateRequestAsync(VisaApplicationCreateRequest request, CancellationToken cancellationToken)
async Task<VisaApplicationModel> IVisaApplicationRequestsHandler.GetApplicationForApplicantAsync(Guid id, CancellationToken cancellationToken)
{
var applicant = await applicants.FindByUserIdAsync(userIdProvider.GetUserId(), cancellationToken);
var application = await applications.GetByApplicantAndApplicationIdAsync(applicant.Id, id, cancellationToken);
var mapped = mapper.Map<VisaApplicationModel>(application);
mapped.Applicant = mapper.Map<ApplicantModel>(applicant);
return mapped;
}
async Task<VisaApplicationModel> IVisaApplicationRequestsHandler.GetApplicationForAuthorityAsync(Guid id, CancellationToken cancellationToken)
{
var pending = await applications.GetPendingApplicationsAsync(cancellationToken);
var application = pending.SingleOrDefault(a => a.Id == id) ?? throw new ApplicationAlreadyProcessedException();
var mapped = mapper.Map<VisaApplicationModel>(application);
var applicant = await applicants.GetByIdAsync(application.ApplicantId, cancellationToken);
mapped.Applicant = mapper.Map<ApplicantModel>(applicant);
return mapped;
}
async Task IVisaApplicationRequestsHandler.HandleCreateRequestAsync(VisaApplicationCreateRequest request, CancellationToken cancellationToken)
{
var applicant = await applicants.FindByUserIdAsync(userIdProvider.GetUserId(), cancellationToken);
@@ -65,6 +70,10 @@ public class VisaApplicationRequestsHandler(
{
var applicantId = await applicants.GetApplicantIdByUserId(userIdProvider.GetUserId(), cancellationToken);
var application = await applications.GetByApplicantAndApplicationIdAsync(applicantId, applicationId, cancellationToken);
if (application.Status is ApplicationStatus.Approved or ApplicationStatus.Rejected)
{
throw new ApplicationAlreadyProcessedException();
}
application.Status = ApplicationStatus.Closed;
await applications.UpdateAsync(application, cancellationToken);
@@ -83,7 +92,7 @@ public class VisaApplicationRequestsHandler(
throw new ApplicationAlreadyProcessedException();
}
ApplicationStatus statusToSet = status switch
var statusToSet = status switch
{
AuthorityRequestStatuses.Approved => ApplicationStatus.Approved,
AuthorityRequestStatuses.Rejected => ApplicationStatus.Rejected,

View File

@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.VisaApplications.Models;
/// Model of past visa for presentation layer
public class PastVisaModel
{
// Date of issue
[Required]
public DateTime IssueDate { get; set; }
/// Name of visa
[MaxLength(ConfigurationConstraints.VisaNameLength)]
[Required]
public string Name { get; set; } = null!;
/// Date when visa expires
[Required]
public DateTime ExpirationDate { get; set; }
}

View File

@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.VisaApplications.Models;
/// Model of past visit for presentation layer
public class PastVisitModel
{
/// First day of past visit
[Required]
public DateTime StartDate { get; set; }
/// Last day of past visit
[Required]
public DateTime EndDate { get; set; }
/// Destination country of past visit
[MaxLength(ConfigurationConstraints.CountryNameLength)]
[Required]
public string DestinationCountry { get; set; } = null!;
}

View File

@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.VisaApplications.Models;
/// Model of permission to destination country for presentation layer
public class PermissionToDestCountryModel
{
/// Date when permission to destination country expires
[Required]
public DateTime ExpirationDate { get; set; }
/// Issuing authority
[MaxLength(ConfigurationConstraints.IssuerNameLength)]
[Required]
public string Issuer { get; set; } = null!;
}

View File

@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.VisaApplications.Models;
/// Model of re-entry permit for presentation layer
public class ReentryPermitModel
{
/// Number of re-entry permit
[MaxLength(ConfigurationConstraints.ReentryPermitNumberLength)]
[Required]
public string Number { get; set; } = null!;
/// Date when re-entry permit expires
[Required]
public DateTime ExpirationDate { get; set; }
}

View File

@@ -0,0 +1,31 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.VisaApplications.Models.Validation;
public class PastVisaModelValidator : AbstractValidator<PastVisaModel>
{
public PastVisaModelValidator(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")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Name of past visa can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.VisaNameLength)
.WithMessage($"Past visa name length must be less than {ConfigurationConstraints.VisaNameLength}");
}
}

View File

@@ -0,0 +1,31 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.VisaApplications.Models.Validation;
public class PastVisitModelValidator : AbstractValidator<PastVisitModel>
{
public PastVisitModelValidator(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")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Destination Country of past visit can contain only english letters, digits and special symbols")
.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 FluentValidation;
namespace ApplicationLayer.Services.VisaApplications.Models.Validation;
public class PermissionToDestCountryModelValidator : AbstractValidator<PermissionToDestCountryModel?>
{
public PermissionToDestCountryModelValidator(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")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Issuer of permission for destination Country can contain only english letters, digits and special symbols")
.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 FluentValidation;
namespace ApplicationLayer.Services.VisaApplications.Models.Validation;
public class ReentryPermitModelValidator : AbstractValidator<ReentryPermitModel?>
{
public ReentryPermitModelValidator(IDateTimeProvider dateTimeProvider)
{
RuleFor(p => p!.Number)
.NotEmpty()
.WithMessage("Re-entry permit number can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Re-entry permit number can contain only english letters, digits and special symbols")
.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,58 @@
using System.ComponentModel.DataAnnotations;
using ApplicationLayer.Services.Applicants.Models;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.Services.VisaApplications.Models;
/// Model of <see cref="VisaApplication" /> with applicant property
public class VisaApplicationModel
{
/// <inheritdoc cref="VisaApplication.Id" />
[Required]
public Guid Id { get; set; }
/// Applicant of application
[Required]
public ApplicantModel Applicant { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.Status" />
[Required]
public ApplicationStatus Status { get; set; }
/// <inheritdoc cref="VisaApplication.ReentryPermit" />
public ReentryPermitModel? ReentryPermit { get; set; }
/// <inheritdoc cref="VisaApplication.DestinationCountry" />
[Required]
public string DestinationCountry { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.PastVisas" />
[Required]
public List<PastVisaModel> PastVisas { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.PermissionToDestCountry" />
public PermissionToDestCountryModel? PermissionToDestCountry { get; set; }
[Required]
public List<PastVisitModel> PastVisits { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.VisaCategory" />
[Required]
public VisaCategory VisaCategory { get; set; }
/// <inheritdoc cref="VisaApplication.ForGroup" />
[Required]
public bool ForGroup { get; set; }
/// <inheritdoc cref="VisaApplication.RequestedNumberOfEntries" />
[Required]
public RequestedNumberOfEntries RequestedNumberOfEntries { get; set; }
/// <inheritdoc cref="VisaApplication.RequestDate" />
[Required]
public DateTime RequestDate { get; set; }
/// <inheritdoc cref="VisaApplication.ValidDaysRequested" />
[Required]
public int ValidDaysRequested { get; set; }
}

View File

@@ -1,43 +0,0 @@
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.Services.VisaApplications.Models;
/// Model of <see cref="VisaApplication"/>
public class VisaApplicationModelForApplicant
{
/// <inheritdoc cref="VisaApplication.Id"/>
public Guid Id { get; set; }
/// <inheritdoc cref="VisaApplication.Status"/>
public ApplicationStatus Status { get; set; }
/// <inheritdoc cref="VisaApplication.ReentryPermit"/>
public ReentryPermit? ReentryPermit { get; set; }
/// <inheritdoc cref="VisaApplication.DestinationCountry"/>
public string DestinationCountry { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.PastVisas"/>
public List<PastVisa> PastVisas { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.PermissionToDestCountry"/>
public PermissionToDestCountry? PermissionToDestCountry { get; set; }
/// <inheritdoc cref="VisaApplication.PastVisits"/>
public List<PastVisit> PastVisits { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.VisaCategory"/>
public VisaCategory VisaCategory { get; set; }
/// <inheritdoc cref="VisaApplication.ForGroup"/>
public bool ForGroup { get; set; }
/// <inheritdoc cref="VisaApplication.RequestedNumberOfEntries"/>
public RequestedNumberOfEntries RequestedNumberOfEntries { get; set; }
/// <inheritdoc cref="VisaApplication.RequestDate"/>
public DateTime RequestDate { get; set; }
/// <inheritdoc cref="VisaApplication.ValidDaysRequested"/>
public int ValidDaysRequested { get; set; }
}

View File

@@ -1,46 +0,0 @@
using ApplicationLayer.Services.Applicants.Models;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.Services.VisaApplications.Models;
/// Model of <see cref="VisaApplication"/> with applicant property
public class VisaApplicationModelForAuthority
{
/// <inheritdoc cref="VisaApplication.Id"/>
public Guid Id { get; set; }
/// Applicant of application
public ApplicantModel Applicant { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.Status"/>
public ApplicationStatus Status { get; set; }
/// <inheritdoc cref="VisaApplication.ReentryPermit"/>
public ReentryPermit? ReentryPermit { get; set; }
/// <inheritdoc cref="VisaApplication.DestinationCountry"/>
public string DestinationCountry { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.PastVisas"/>
public List<PastVisa> PastVisas { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.PermissionToDestCountry"/>
public PermissionToDestCountry? PermissionToDestCountry { get; set; }
public List<PastVisit> PastVisits { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.VisaCategory"/>
public VisaCategory VisaCategory { get; set; }
/// <inheritdoc cref="VisaApplication.ForGroup"/>
public bool ForGroup { get; set; }
/// <inheritdoc cref="VisaApplication.RequestedNumberOfEntries"/>
public RequestedNumberOfEntries RequestedNumberOfEntries { get; set; }
/// <inheritdoc cref="VisaApplication.RequestDate"/>
public DateTime RequestDate { get; set; }
/// <inheritdoc cref="VisaApplication.ValidDaysRequested"/>
public int ValidDaysRequested { get; set; }
}

View File

@@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.Services.VisaApplications.Models;
/// Model of <see cref="VisaApplication" />
public class VisaApplicationPreview
{
/// <inheritdoc cref="VisaApplication.Id" />
[Required]
public Guid Id { get; set; }
/// <inheritdoc cref="VisaApplication.Status" />
[Required]
public ApplicationStatus Status { get; set; }
/// <inheritdoc cref="VisaApplication.DestinationCountry" />
[Required]
public string DestinationCountry { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.VisaCategory" />
[Required]
public VisaCategory VisaCategory { get; set; }
/// <inheritdoc cref="VisaApplication.RequestDate" />
[Required]
public DateTime RequestDate { get; set; }
/// <inheritdoc cref="VisaApplication.ValidDaysRequested" />
[Required]
public int ValidDaysRequested { get; set; }
}

View File

@@ -1,27 +0,0 @@
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

@@ -1,30 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,5 +1,6 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.Applicants.NeededServices;
using ApplicationLayer.Services.VisaApplications.Models;
using Domains;
using Domains.VisaApplicationDomain;
using FluentValidation;
@@ -9,17 +10,23 @@ 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,
IValidator<ReentryPermitModel?> reentryPermitModelValidator,
IValidator<PastVisaModel> pastVisaModelValidator,
IValidator<PermissionToDestCountryModel?> permissionToDestCountryModelValidator,
IValidator<PastVisitModel> pastVisitModelValidator,
IApplicantsRepository applicants,
IUserIdProvider userIdProvider)
{
RuleFor(r => r.PermissionToDestCountry)
.NotEmpty()
.WithMessage("For transit you must provide permission to destination country")
.SetValidator(permissionToDestCountryModelValidator)
.When(r => r.VisaCategory is VisaCategory.Transit);
RuleFor(r => r.ReentryPermit)
.NotEmpty()
.WithMessage("Non-residents must provide re-entry permission")
.SetValidator(reentryPermitValidator)
.SetValidator(reentryPermitModelValidator)
.WhenAsync(async (_, ct) =>
await applicants.IsApplicantNonResidentByUserId(userIdProvider.GetUserId(), ct));
@@ -40,14 +47,9 @@ public class VisaApplicationCreateRequestValidator : AbstractValidator<VisaAppli
.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));
.SetValidator(pastVisaModelValidator);
RuleForEach(r => r.PastVisits)
.SetValidator(pastVisitValidator);
.SetValidator(pastVisitModelValidator);
}
}

View File

@@ -1,16 +1,37 @@
using Domains.VisaApplicationDomain;
using System.ComponentModel.DataAnnotations;
using ApplicationLayer.Services.VisaApplications.Models;
using Domains;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.Services.VisaApplications.Requests;
/// Model of visa request from user
public record VisaApplicationCreateRequest(
ReentryPermit? ReentryPermit,
string DestinationCountry,
VisaCategory VisaCategory,
bool IsForGroup,
RequestedNumberOfEntries RequestedNumberOfEntries,
int ValidDaysRequested,
PastVisa[] PastVisas,
PermissionToDestCountry? PermissionToDestCountry,
PastVisit[] PastVisits
);
public class VisaApplicationCreateRequest
{
public ReentryPermitModel? ReentryPermit { get; set; }
[Required]
[MaxLength(ConfigurationConstraints.CountryNameLength)]
public string DestinationCountry { get; set; } = null!;
[Required]
public VisaCategory VisaCategory { get; set; }
[Required]
public bool IsForGroup { get; set; }
[Required]
public RequestedNumberOfEntries RequestedNumberOfEntries { get; set; }
[Required]
[Range(0, ConfigurationConstraints.MaxValidDays)]
public int ValidDaysRequested { get; set; }
[Required]
public PastVisaModel[] PastVisas { get; set; } = null!;
public PermissionToDestCountryModel? PermissionToDestCountry { get; set; }
[Required]
public PastVisitModel[] PastVisits { get; set; } = null!;
}

View File

@@ -0,0 +1,16 @@
@using BlazorWebAssemblyVisaApiClient.ErrorHandling
<GlobalErrorHandler >
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</GlobalErrorHandler>

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="Blazor.Bootstrap" Version="3.0.0" />
<PackageReference Include="FluentValidation" Version="11.9.2" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.DataAnnotations.Validation" Version="3.2.0-rc1.20223.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.1" PrivateAssets="all"/>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\VisaApiClient\VisaApiClient.csproj" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json" />
<_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css.map" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,4 @@
namespace BlazorWebAssemblyVisaApiClient.Common.Exceptions
{
public class BlazorClientException(string message) : Exception(message);
}

View File

@@ -0,0 +1,4 @@
namespace BlazorWebAssemblyVisaApiClient.Common.Exceptions
{
public class NotLoggedInException() : BlazorClientException("User is not logged in.");
}

View File

@@ -0,0 +1,81 @@
@using System.Net
@using BlazorWebAssemblyVisaApiClient.ErrorHandling
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
@using VisaApiClient
@code {
public static AuthData? AuthData;
[CascadingParameter] private GlobalErrorHandler ErrorHandler { get; set; } = null!;
[CascadingParameter] private Status? Status { get; set; }
[Inject] private Client Client { get; set; } = null!;
[Inject] private NavigationManager Nav { get; set; } = null!;
[Inject] private IUserDataProvider UserDataProvider { get; set; } = null!;
///Authorize with email and password
/// <returns>Message to user</returns>
public async Task TryAuthorize(AuthData authData)
{
Status?.SetMessage("Wait...");
try
{
var token = await Client.LoginAsync(authData.Email, authData.Password);
Client.AuthToken = token;
AuthData = authData;
UserDataProvider.UpdateCurrentRole();
Status?.SetSuccess("Logged in successfully.");
}
catch (ApiException<ProblemDetails> e)
{
if (e.Result.Status == (int)HttpStatusCode.Forbidden)
{
Status?.SetError(e.Result.Detail!);
}
else
{
Status?.SetError("Error occured");
ErrorHandler.Handle(e);
}
}
catch (Exception e)
{
Status?.SetError("Error occured");
ErrorHandler.Handle(e);
}
}
public void Logout()
{
Client.AuthToken = null;
AuthData = null;
try
{
UserDataProvider.UpdateCurrentRole();
}
catch (Exception e)
{
ErrorHandler.Handle(e);
}
}
///Re-auth if token expired or something
public async Task ReAuthenticate()
{
if (AuthData is not null)
{
await TryAuthorize(AuthData);
}
else
{
Client.AuthToken = null;
AuthData = null;
Nav.NavigateTo("/");
}
}
}

View File

@@ -0,0 +1,9 @@
@using BlazorWebAssemblyVisaApiClient.ErrorHandling
@using VisaApiClient
@code
{
[CascadingParameter] protected GlobalErrorHandler ErrorHandler { get; set; } = null!;
[Inject] protected Client Client { get; set; } = null!;
}

View File

@@ -0,0 +1,39 @@
@using VisaApiClient
<div>
<div >
<label >
Country:<br/>
<InputText class="rounded" @bind-Value="Address.Country"/>
</label><br/>
<ValidationMessage For="() => Address.Country"></ValidationMessage><br/>
</div>
<div >
<label >
City:<br/>
<InputText class="rounded" @bind-Value="Address.City"/>
</label><br/>
<ValidationMessage For="() => Address.City"></ValidationMessage><br/>
</div>
<div >
<label >
Street:<br/>
<InputText class="rounded" @bind-Value="Address.Street"/>
</label><br/>
<ValidationMessage For="() => Address.Street"></ValidationMessage><br/>
</div>
<div >
<label >
Building:<br/>
<InputText class="rounded" @bind-Value="Address.Building"/>
</label><br/>
<ValidationMessage For="() => Address.Building"></ValidationMessage>
</div>
</div>
@code {
[Parameter, EditorRequired] public AddressModel Address { get; set; } = null!;
}

View File

@@ -0,0 +1,23 @@
@using VisaApiClient
<div>
<div >
<label >
Email:<br/>
<InputText class="rounded" @bind-Value="AuthData.Email"/>
</label><br/>
<ValidationMessage For="() => AuthData.Email"></ValidationMessage><br/>
</div>
<div >
<label >
Password:<br/>
<InputText class="rounded" @bind-Value="AuthData.Password"/>
</label><br/>
<ValidationMessage For="() => AuthData.Password"></ValidationMessage>
</div>
</div>
@code {
[Parameter, EditorRequired] public AuthData AuthData { get; set; } = null!;
}

View File

@@ -0,0 +1,51 @@
@using System.Linq.Expressions
@using System.Reflection
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
@typeparam TItem where TItem : class
@typeparam TMember where TMember : struct, Enum
<InputSelect TValue="TMember" @bind-Value="selected">
@foreach (var value in enumValues)
{
<option value="@value.Key">@value.Value</option>
}
</InputSelect><br/>
@code {
[Parameter, EditorRequired] public TItem Model { get; set; } = default!;
[Parameter, EditorRequired] public Expression<Func<TItem, TMember>> EnumProperty { get; set; } = null!;
[Parameter] public Action? OnChanged { get; set; }
private Dictionary<TMember, string> enumValues = new();
private PropertyInfo modelMemberInfo = null!;
private TMember selected;
protected override void OnInitialized()
{
var modelMemberName = ((MemberExpression)EnumProperty.Body).Member.Name;
modelMemberInfo = typeof(TItem).GetProperty(modelMemberName)!;
foreach (var value in Enum.GetValues<TMember>())
{
enumValues.Add(value, value.GetDisplayName());
}
}
protected override void OnAfterRender(bool firstRender)
{
var current = (TMember)modelMemberInfo.GetValue(Model)!;
if (!current.Equals(selected))
{
OnValueChanged();
}
}
private void OnValueChanged()
{
modelMemberInfo.SetValue(Model, selected);
OnChanged?.Invoke();
}
}

View File

@@ -0,0 +1,31 @@
@using VisaApiClient
<div>
<div >
<label>
First name@(Constants.RequiredFieldMarkup):<br/>
<InputText DisplayName="First name" class="rounded" @bind-Value="Name.FirstName"/>
</label><br/>
<ValidationMessage For="() => Name.FirstName"></ValidationMessage>
</div><br/>
<div >
<label>
Surname@(Constants.RequiredFieldMarkup):<br/>
<InputText class="rounded" @bind-Value="Name.Surname"/>
</label><br/>
<ValidationMessage For="() => Name.Surname"></ValidationMessage>
</div><br/>
<div >
<label>
Patronymic:<br/>
<InputText class="rounded" @bind-Value="Name.Patronymic"/>
</label><br/>
<ValidationMessage For="() => Name.Patronymic"></ValidationMessage>
</div>
</div>
@code {
[Parameter, EditorRequired] public NameModel Name { get; set; } = null!;
}

View File

@@ -0,0 +1,52 @@
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider
@using VisaApiClient
<div>
<div >
<label>
Passport number:<br/>
<InputText DisplayName="Passport number" class="rounded" @bind-Value="Passport.Number"/>
</label><br/>
<ValidationMessage For="() => Passport.Number"></ValidationMessage>
</div><br/>
<div >
<label>
Issuer:<br/>
<InputText class="rounded" @bind-Value="Passport.Issuer"/>
</label><br/>
<ValidationMessage For="() => Passport.Issuer"></ValidationMessage>
</div><br/>
<div >
<label>
Issue date:<br/>
<InputDate DisplayName="Issue date" class="rounded" @bind-Value="Passport.IssueDate" max="@formattedDate"/>
</label><br/>
<ValidationMessage For="() => Passport.IssueDate"></ValidationMessage>
</div><br/>
<div >
<label>
Expiration date:<br/>
<InputDate DisplayName="Expiration date" class="rounded" @bind-Value="Passport.ExpirationDate" min="@formattedDate"/>
</label><br/>
<ValidationMessage For="() => Passport.ExpirationDate"></ValidationMessage>
</div>
</div>
@code {
private string formattedDate = null!;
[Parameter, EditorRequired] public PassportModel Passport { get; set; } = null!;
[Inject] IDateTimeProvider DateTimeProvider { get; set; } = null!;
protected override void OnInitialized()
{
Passport.IssueDate = DateTime.Now;
Passport.ExpirationDate = DateTime.Now;
formattedDate = DateTimeProvider.FormattedNow();
}
}

View File

@@ -0,0 +1,22 @@
@using BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models
<div>
<div >
<label >
Name:<br/>
<InputText class="rounded" @bind-Value="PlaceOfWork.Name"/>
</label><br/>
<ValidationMessage For="() => PlaceOfWork.Name"></ValidationMessage><br/>
</div>
<div >
<label >
Phone number:<br/>
<InputText DisplayName="Phone number" class="rounded" @bind-Value="PlaceOfWork.PhoneNum"/>
</label><br/>
<ValidationMessage For="() => PlaceOfWork.PhoneNum"></ValidationMessage>
</div>
</div>
@code {
[Parameter, EditorRequired] public PlaceOfWorkModel PlaceOfWork { get; set; } = null!;
}

View File

@@ -0,0 +1,33 @@
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider
@using VisaApiClient
<div>
<label>
Issuer:<br/>
<InputText DisplayName="Issuer of permission to destination Country" class="rounded"
@bind-Value="PermissionToDestCountry.Issuer"/>
</label><br/>
<ValidationMessage For="() => PermissionToDestCountry.Issuer"></ValidationMessage><br/>
<label>
Expiration date:<br/>
<InputDate DisplayName="Expiration date of permission to destination Country" class="rounded"
@bind-Value="PermissionToDestCountry.ExpirationDate"
min="@formattedDate"/>
</label><br/>
<ValidationMessage For="() => PermissionToDestCountry.ExpirationDate"></ValidationMessage>
</div>
@code {
private string formattedDate = null!;
[Parameter, EditorRequired] public PermissionToDestCountryModel PermissionToDestCountry { get; set; } = null!;
[Inject] IDateTimeProvider DateTimeProvider { get; set; } = null!;
protected override void OnInitialized()
{
formattedDate = DateTimeProvider.FormattedNow();
}
}

View File

@@ -0,0 +1,34 @@
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider
@using VisaApiClient
<div>
<label>
Number:<br/>
<InputText DisplayName="Number of re-entry permit" class="rounded"
@bind-Value="ReentryPermit.Number"/>
</label><br/>
<ValidationMessage For="() => ReentryPermit.Number"></ValidationMessage><br/>
<label>
Expiration date:<br/>
<InputDate DisplayName="Expiration date of re-entry permit" class="rounded"
@bind-Value="ReentryPermit.ExpirationDate"
min="@formattedDate"/>
</label><br/>
<ValidationMessage For="() => ReentryPermit.ExpirationDate"></ValidationMessage>
</div>
@code {
private string formattedDate = null!;
[Parameter, EditorRequired] public ReentryPermitModel ReentryPermit { get; set; } = null!;
[Inject] IDateTimeProvider DateTimeProvider { get; set; } = null!;
protected override void OnInitialized()
{
formattedDate = DateTimeProvider.FormattedNow();
ReentryPermit.ExpirationDate = DateTimeProvider.Now();
}
}

View File

@@ -0,0 +1,34 @@
<p class="@statusClass">@((MarkupString)StatusText)</p>
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
@code {
private string statusClass = string.Empty;
[Parameter]
public RenderFragment? ChildContent { get; set; }
public string StatusText { get; private set; } = string.Empty;
public void SetMessage(string message)
{
statusClass = string.Empty;
StatusText = message;
StateHasChanged();
}
public void SetError(string message)
{
statusClass = "validation-message";
StatusText = message;
StateHasChanged();
}
public void SetSuccess(string message)
{
statusClass = "text-success";
StatusText = message;
StateHasChanged();
}
}

View File

@@ -0,0 +1,20 @@
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Components;
namespace BlazorWebAssemblyVisaApiClient
{
public static class Constants
{
public readonly static Regex EnglishWordRegex = new("^[a-zA-Z]*$");
public readonly static Regex EnglishPhraseRegex = new(@"^[a-zA-Z№0-9?><;,{}[\]\-_+=!@#$%\^&*|']*$");
public readonly static Regex PhoneNumRegex = new(@"^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$");
public readonly static MarkupString RequiredFieldMarkup = (MarkupString)"<span style=\"color: red;\">*</span>";
public const string ApplicantRole = "Applicant";
public const string ApprovingAuthorityRole = "ApprovingAuthority";
public const string AdminRole = "Admin";
}
}

View File

@@ -0,0 +1,53 @@
@using System.Net
@using BlazorWebAssemblyVisaApiClient.Common.Exceptions
@using VisaApiClient
<CascadingValue Value="this">
<Modal @ref="modal">
<BodyTemplate>
@errorDetails
</BodyTemplate>
<FooterTemplate>
<Button Color="ButtonColor.Secondary" @onclick="modal.HideAsync">Okaaaay</Button>
</FooterTemplate>
</Modal>
@ChildContent
</CascadingValue>
@code
{
private Modal modal = null!;
private string errorDetails = null!;
[Parameter] public RenderFragment? ChildContent { get; set; }
[Inject] private NavigationManager Nav { get; set; } = null!;
public void Handle(Exception ex)
{
switch (ex)
{
case ApiException<ProblemDetails>
{
StatusCode: (int)HttpStatusCode.Unauthorized or (int)HttpStatusCode.Forbidden
} or NotLoggedInException:
Nav.NavigateTo("/");
modal.Title = "Authorization failed";
errorDetails = "You are not authorized or your authorization is expired";
modal.ShowAsync();
break;
case ApiException<ProblemDetails> problemDetails:
modal.Title = problemDetails.Result.Title!;
errorDetails = problemDetails.Result.Detail!;
modal.ShowAsync();
break;
default:
modal.Title = "Something went wrong";
errorDetails = "Please, text an email with your problem definition on nasrudin@mail.ru";
modal.ShowAsync();
break;
}
}
}

View File

@@ -0,0 +1,19 @@
using AutoMapper;
using BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models;
using VisaApiClient;
using PlaceOfWorkModel = BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models.PlaceOfWorkModel;
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.AutoMapper.Profiles
{
public class RegisterApplicantRequestProfile : Profile
{
public RegisterApplicantRequestProfile()
{
CreateMap<RegisterApplicantRequestModel, RegisterApplicantRequest>(MemberList.Destination);
CreateMap<RegisterRequestModel, RegisterRequest>(MemberList.Destination);
CreateMap<PlaceOfWorkModel, VisaApiClient.PlaceOfWorkModel>(MemberList.Destination);
}
}
}

View File

@@ -0,0 +1,14 @@
using AutoMapper;
using BlazorWebAssemblyVisaApiClient.Validation.VisaApplications.Models;
using VisaApiClient;
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.AutoMapper.Profiles
{
public class VisaApplicationCreateRequestProfile : Profile
{
public VisaApplicationCreateRequestProfile()
{
CreateMap<VisaApplicationCreateRequestModel, VisaApplicationCreateRequest>(MemberList.Destination);
}
}
}

View File

@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
{
public static class EnumExtensions
{
public static string GetDisplayName(this Enum value)
{
var enumMembers = value.GetType().GetMembers();
var member = enumMembers.First(info => info.Name == value.ToString());
var displayAttribute = (DisplayAttribute?)member
.GetCustomAttributes(typeof(DisplayAttribute), false)
.FirstOrDefault();
var displayName = displayAttribute?.Name ?? value.ToString();
return displayName;
}
}
}

View File

@@ -0,0 +1,22 @@
using System.Text;
using FluentValidation.Results;
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
{
public static class ValidationResultExtensions
{
public static string ToErrorsString(this ValidationResult validationResult)
=> ErrorsToString(validationResult.Errors.Select(e => e.ErrorMessage));
private static string ErrorsToString(IEnumerable<string> errors)
{
var stringBuilder = new StringBuilder();
foreach (var error in errors)
{
stringBuilder.Append($"{error}<br/>");
}
return stringBuilder.ToString();
}
}
}

View File

@@ -0,0 +1,9 @@
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider
{
public class DateTimeProvider : IDateTimeProvider
{
public DateTime Now() => DateTime.Now;
public string FormattedNow() => Now().ToString("yyyy-MM-dd");
}
}

View File

@@ -0,0 +1,9 @@
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider
{
public interface IDateTimeProvider
{
DateTime Now();
string FormattedNow();
}
}

View File

@@ -0,0 +1,6 @@
using BlazorWebAssemblyVisaApiClient.Common.Exceptions;
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider.Exceptions
{
public class UnknownRoleException() : BlazorClientException("Unknown user role");
}

View File

@@ -0,0 +1,15 @@
using VisaApiClient;
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
{
public interface IUserDataProvider
{
public string? CurrentRole { get; }
public Action? OnRoleChanged { get; set; }
public Task<ApplicantModel> GetApplicant();
public void UpdateCurrentRole();
}
}

View File

@@ -0,0 +1,53 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider.Exceptions;
using VisaApiClient;
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
{
public class UserDataProvider(Client client) : IUserDataProvider
{
private readonly static JwtSecurityTokenHandler tokenHandler = new();
public string? CurrentRole { get; private set; }
public Action? OnRoleChanged { get; set; }
public async Task<ApplicantModel> GetApplicant()
{
return await client.GetApplicantAsync();
}
public void UpdateCurrentRole()
{
var role = CurrentRole;
if (client.AuthToken is null)
{
if (CurrentRole is not null)
{
role = null;
}
}
else
{
var token = tokenHandler.ReadJwtToken(client.AuthToken.Token);
role = token.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Role)?.Value;
switch (role)
{
case Constants.ApplicantRole: break;
case Constants.ApprovingAuthorityRole: break;
case Constants.AdminRole: break;
default: throw new UnknownRoleException();
}
}
if (CurrentRole != role)
{
CurrentRole = role;
OnRoleChanged?.Invoke();
}
}
}
}

View File

@@ -0,0 +1,41 @@
@using BlazorWebAssemblyVisaApiClient.Components.Auth
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu/>
</div>
<main class="fullscreen">
<div class="top-row px-4">
<AuthComponent @ref="authComponent"/>
@if (UserDataProvider.CurrentRole is not null)
{
<p>
Logged as @UserDataProvider.CurrentRole (@AuthComponent.AuthData?.Email)
<button class="btn-secondary" @onclick="authComponent.Logout">Log out</button>
</p>
}
else
{
<NavLink href="/">Log in</NavLink>
}
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
@code
{
private AuthComponent authComponent = null!;
[Inject] private IUserDataProvider UserDataProvider { get; set; } = null!;
protected override void OnInitialized()
{
UserDataProvider.OnRoleChanged += StateHasChanged;
}
}

View File

@@ -0,0 +1,77 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}

View File

@@ -0,0 +1,79 @@
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">Schengen Visa</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Login
</NavLink>
</div>
</nav>
@if (UserDataProvider.CurrentRole is Constants.ApplicantRole or Constants.ApprovingAuthorityRole)
{
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="applications" Match="NavLinkMatch.All">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Applications
</NavLink>
</div>
</nav>
}
@if (UserDataProvider.CurrentRole is Constants.ApplicantRole)
{
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="applications/new" Match="NavLinkMatch.All">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> New application
</NavLink>
</div>
</nav>
}
@if (UserDataProvider.CurrentRole is Constants.AdminRole)
{
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="authorities" Match="NavLinkMatch.All">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Authorities
</NavLink>
</div>
</nav>
}
@if (UserDataProvider.CurrentRole is Constants.AdminRole)
{
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="authorities/add" Match="NavLinkMatch.All">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Add authority
</NavLink>
</div>
</nav>
}
</div>
@code {
private bool collapseNavMenu = true;
private string? currentRole = null!;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
[Inject] private IUserDataProvider UserDataProvider { get; set; } = null!;
protected override void OnInitialized()
{
UserDataProvider.OnRoleChanged += StateHasChanged;
}
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}

View File

@@ -0,0 +1,83 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -0,0 +1,60 @@
@page "/authorities/add"
@using AutoMapper
@using BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models
@using VisaApiClient
@using BlazorWebAssemblyVisaApiClient.Components
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
@using FluentValidation
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
<EditForm Model="requestModel" class="with-centered-content">
<ObjectGraphDataAnnotationsValidator/>
<div >
<label>
Email:<br/>
<InputText class="rounded" @bind-Value="requestModel.AuthData.Email"/>
<ValidationMessage For="() => requestModel.AuthData.Email"/>
</label><br/>
<p/>
<label>
Password:<br/>
<InputText class="rounded" @bind-Value="requestModel.AuthData.Password"/>
<ValidationMessage For="() => requestModel.AuthData.Password"/>
</label><br/>
<p/>
<button class="btn-primary rounded" @onclick="Add">Add</button><br/>
<Status @ref="status"/>
</div>
</EditForm>
@code
{
private RegisterRequestModel requestModel = new();
private Status status = new();
[Inject] private IValidator<RegisterRequestModel> RegisterRequestModelValidator { get; set; } = null!;
[Inject] private IMapper Mapper { get; set; } = null!;
private async Task Add()
{
var validationResult = await RegisterRequestModelValidator.ValidateAsync(requestModel);
if (!validationResult.IsValid)
{
status.SetError(validationResult.ToErrorsString());
return;
}
try
{
status.SetMessage("Wait...");
await Client.RegisterAuthorityAsync(Mapper.Map<RegisterRequest>(requestModel));
status.SetSuccess("Success");
}
catch (Exception e)
{
ErrorHandler.Handle(e);
}
}
}

View File

@@ -0,0 +1,259 @@
@page "/applications/{ApplicationId}"
@using System.Net
@using BlazorWebAssemblyVisaApiClient.Common.Exceptions
@using BlazorWebAssemblyVisaApiClient.Components
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
@using VisaApiClient
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
<PageTitle>Application</PageTitle>
<table class="table table-bordered table-hover table-sm">
<tbody>
<tr>
<td >
Applicant's fullname:<br/>
<em>@NameToString(application.Applicant.Name)</em>
</td>
<td colspan="2">
Date of birth:<br/>
<em>@application.Applicant.BirthDate.ToString("d")</em>
</td>
</tr>
<tr>
<td colspan="3">
Country and city of birth:<br/>
<em>@application.Applicant.Passport.Number</em>
</td>
</tr>
<tr>
<td colspan="2">
Citizenship:<br/>
<em>@application.Applicant.Citizenship</em>
</td>
<td >
Citizenship by birth:<br/>
<em>@application.Applicant.CitizenshipByBirth</em>
</td>
</tr>
<tr>
<td >
Gender:<br/>
<em>@application.Applicant.Gender.GetDisplayName()</em>
</td>
<td >
Marital status:<br/>
<em>@application.Applicant.MaritalStatus.GetDisplayName()</em>
</td>
</tr>
<tr>
<td >
Father's fullname:<br/>
<em>@NameToString(application.Applicant.FatherName)</em>
</td>
<td colspan="2">
Mother's fullname:<br/>
<em>@NameToString(application.Applicant.MotherName)</em>
</td>
</tr>
<tr>
<td >
Passport number:<br/>
<em>@application.Applicant.Passport.Number</em>
</td>
<td >
Issue date:<br/>
<em>@application.Applicant.Passport.IssueDate.ToString("d")</em>
</td>
<td >
Expiration date:<br/>
<em>@application.Applicant.Passport.ExpirationDate.ToString("d")</em>
</td>
</tr>
<tr>
<td colspan="3">
Passport issuer:<br/>
<em>@application.Applicant.Passport.Issuer</em>
</td>
</tr>
<tr>
<td colspan="3">
Re-entry permission (for non-residents):<br/>
@if (application.Applicant.IsNonResident)
{
<em>@(application.ReentryPermit is null ? "None" : $"{application.ReentryPermit.Number}, expires at {application.ReentryPermit.ExpirationDate:d}")</em>
}
else
{
<em>Not non-resident</em>
}
</td>
</tr>
<tr>
<td colspan="3">
Job title:<br/>
<em>@application.Applicant.JobTitle</em>
</td>
</tr>
<tr>
<td colspan="3">
Place of work, address, hirer's phone number:<br/>
<em>
@((MarkupString)$"{application.Applicant.PlaceOfWork.Name}<br>Address: {AddressToString(application.Applicant.PlaceOfWork.Address)}<br>Phone num: {application.Applicant.PlaceOfWork.PhoneNum}")
</em>
</td>
</tr>
<tr>
<td >
Destination Country:<br/>
<em>@application.DestinationCountry</em>
</td>
<td >
Visa category:<br/>
<em>@application.VisaCategory</em>
</td>
<td >
Visa:<br/>
<em>@(application.ForGroup ? "For group" : "Individual")</em>
</td>
</tr>
<tr>
<td >
Requested number of entries:<br/>
<em>@application.RequestedNumberOfEntries.GetDisplayName()</em>
</td>
<td colspan="2">
Valid for:<br/>
<em>@($"{application.ValidDaysRequested} days")</em>
</td>
</tr>
<tr>
<td colspan="3">
Past visas:<br/>
@if (application.PastVisas.Any())
{
foreach (var visa in application.PastVisas)
{
<em>@($"{visa.Name} issued at {visa.IssueDate:d} and was valid until {visa.ExpirationDate:d}")</em>
<br/>
}
}
else
{
<em>None</em>
}
</td>
</tr>
<tr>
<td colspan="3">
Permission to destination Country, if transit:<br/>
@if (application.VisaCategory is VisaCategory.Transit)
{
<em>@(application.PermissionToDestCountry is null ? "None" : $"Expires at {application.PermissionToDestCountry.ExpirationDate}, issued by: {application.PermissionToDestCountry.Issuer}")</em>
}
else
{
<em>Non-transit</em>
}
</td>
</tr>
<tr>
<td colspan="3">
Past visits:<br/>
@if (application.PastVisas.Any())
{
foreach (var visit in application.PastVisits)
{
<em>@($"Visit to {visit.DestinationCountry}, entered at {visit.StartDate:d} and lasts until {visit.EndDate:d}")</em>
<br/>
}
}
else
{
<em>None</em>
}
</td>
</tr>
</tbody>
</table>
@if (currentRole == Constants.ApprovingAuthorityRole)
{
<button class="btn-outline-primary" @onclick="Approve">Approve</button>
<button class="btn-outline-danger" @onclick="Reject">Reject</button>
<Status @ref="status"/>
}
@code {
private VisaApplicationModel application = new();
private string currentRole = null!;
private Status status = null!;
[Parameter] public string ApplicationId { get; set; } = null!;
[Inject] private IUserDataProvider UserDataProvider { get; set; } = null!;
[Inject] private NavigationManager Nav { get; set; } = null!;
protected override async Task OnInitializedAsync()
{
try
{
var applicationId = Guid.Parse(ApplicationId);
currentRole = UserDataProvider.CurrentRole ?? throw new NotLoggedInException();
application = currentRole switch
{
Constants.ApplicantRole => await Client.GetApplicationForApplicantAsync(applicationId),
Constants.ApprovingAuthorityRole => await Client.GetApplicationForAuthorityAsync(applicationId),
_ => throw new NotLoggedInException()
};
}
catch (Exception e)
{
if (e is ApiException<ProblemDetails> { Result.Status: (int)HttpStatusCode.Conflict })
{
Nav.NavigateTo("/applications");
}
ErrorHandler.Handle(e);
}
}
private static string NameToString(NameModel name)
=> $"{name.FirstName} {name.Surname} {name.Patronymic}".TrimEnd();
private static string AddressToString(AddressModel address)
=> $"{address.Country}, {address.City}, {address.Street} {address.Building}";
private async void Approve()
{
try
{
status.SetMessage("Wait...");
await Client.SetStatusFromAuthorityAsync(application.Id, AuthorityRequestStatuses.Approved);
Nav.NavigateTo("/applications");
}
catch (Exception e)
{
status.SetError("Error occured.");
ErrorHandler.Handle(e);
}
}
private async void Reject()
{
try
{
status.SetMessage("Wait...");
await Client.SetStatusFromAuthorityAsync(application.Id, AuthorityRequestStatuses.Rejected);
Nav.NavigateTo("/applications");
}
catch (Exception e)
{
status.SetError("Error occured.");
ErrorHandler.Handle(e);
}
}
}

View File

@@ -0,0 +1,105 @@
@page "/applications"
@using BlazorWebAssemblyVisaApiClient.Common.Exceptions
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
@using VisaApiClient
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
<PageTitle>Applications</PageTitle>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Destination Country</th>
<th>Visa Category</th>
<th>Request date</th>
<th>Days requested</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var application in applications)
{
var rowClass = application.Status switch
{
ApplicationStatus.Pending => "",
ApplicationStatus.Approved => "table-success",
ApplicationStatus.Rejected => "table-danger",
ApplicationStatus.Closed => "table-danger",
_ => throw new ArgumentOutOfRangeException()
};
<tr class="@rowClass">
<td>@application.DestinationCountry</td>
<td>@application.VisaCategory.GetDisplayName()</td>
<td>@application.RequestDate.ToString("d")</td>
<td>@application.ValidDaysRequested</td>
<td>@application.Status.GetDisplayName()</td>
<td>
<NavLink href="@($"/applications/{application.Id}")">
<button class="btn-outline-primary">See</button>
</NavLink>
@if (currentRole == Constants.ApplicantRole && application.Status is ApplicationStatus.Pending)
{
<span> | </span>
<input type="button" class="border-danger" @onclick="() => CloseApplication(application)" value="Close"/>
}
</td>
</tr>
}
</tbody>
</table >
@code {
private string currentRole = null!;
private List<VisaApplicationPreview> applications = [];
[Inject] private IUserDataProvider UserDataProvider { get; set; } = null!;
protected override async Task OnInitializedAsync()
{
try
{
currentRole = UserDataProvider.CurrentRole ?? throw new NotLoggedInException();
}
catch (Exception e)
{
ErrorHandler.Handle(e);
}
await Fetch();
}
private async Task Fetch()
{
try
{
applications = currentRole switch
{
Constants.ApplicantRole => (await Client.GetApplicationsForApplicantAsync()).OrderByDescending(a => a.RequestDate).ToList(),
Constants.ApprovingAuthorityRole => (await Client.GetPendingAsync()).OrderByDescending(a => a.RequestDate).ToList(),
_ => throw new NotLoggedInException()
};
}
catch (Exception e)
{
ErrorHandler.Handle(e);
}
}
private async Task CloseApplication(VisaApplicationPreview application)
{
try
{
await Client.CloseApplicationAsync(application.Id);
application.Status = ApplicationStatus.Closed;
StateHasChanged();
}
catch (Exception e)
{
ErrorHandler.Handle(e);
}
}
}

View File

@@ -0,0 +1,32 @@
@page "/"
@using BlazorWebAssemblyVisaApiClient.Components.Auth
@using VisaApiClient
@using BlazorWebAssemblyVisaApiClient.Components.FormComponents.Applicants
@using BlazorWebAssemblyVisaApiClient.Components
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
<PageTitle>Authentication</PageTitle>
<div class="with-centered-content">
<EditForm class="form" Model="loginData" OnValidSubmit="TryLogin">
<DataAnnotationsValidator/>
<AuthDataInput AuthData="loginData"/><br/>
<input class="btn-outline-primary rounded" type="submit" value="Login"/>
or
<NavLink href="register">Register</NavLink >
<Status><AuthComponent @ref="auth"/></Status>
</EditForm>
</div>
@code
{
private AuthData loginData = new();
private AuthComponent auth = null!;
private async Task TryLogin()
{
await auth.TryAuthorize(loginData);
}
}

View File

@@ -0,0 +1,58 @@
@page "/authorities"
@using VisaApiClient
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Email</th><th></th>
</tr>
</thead>
<tbody>
@foreach (var authority in authorities)
{
var path = $"authorities/{authority.Id}/{authority.Email}";
<tr>
<td>@authority.Email</td>
<td>
<NavLink href="@path">
<button class="btn-outline-primary">Change</button>
</NavLink>
|
<button class="btn-outline-danger" @onclick="() => Delete(authority)">Delete</button>
</td>
</tr>
}
</tbody>
</table>
@code {
private List<UserModel> authorities = [];
protected override async Task OnInitializedAsync()
{
try
{
authorities = (await Client.GetAuthorityAccountsAsync()).ToList();
}
catch (Exception e)
{
ErrorHandler.Handle(e);
}
}
private async Task Delete(UserModel authority)
{
try
{
await Client.RemoveAuthorityAccountAsync(authority.Id);
authorities.Remove(authority);
StateHasChanged();
}
catch (Exception e)
{
ErrorHandler.Handle(e);
}
}
}

View File

@@ -0,0 +1,83 @@
@page "/authorities/{authorityId}/{oldEmail}"
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
@using BlazorWebAssemblyVisaApiClient.Common.Exceptions
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
@using VisaApiClient
@using BlazorWebAssemblyVisaApiClient.Components
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
@using FluentValidation
<EditForm Model="model" class="with-centered-content">
<div >
<label>
New email:<br/>
<InputText class="rounded" @bind-Value="model.Email"/>
</label><br/><p/>
<label>
New password (leave blank if shouldn't be changed):<br/>
<InputText class="rounded" @bind-Value="model.Password"/>
</label><br/><p/>
<button class="btn-primary rounded" @onclick="Save">Save</button><br/>
<Status @ref="status"/>
</div>
</EditForm>
@code
{
private Status status = null!;
private ChangeAuthData model = new();
[Parameter] public string AuthorityId { get; set; } = null!;
[Parameter] public string OldEmail { get; set; } = null!;
[Inject] private IUserDataProvider UserDataProvider { get; set; } = null!;
[Inject] private IValidator<ChangeUserAuthDataRequest> ChangeUserAuthDataRequestValidator { get; set; } = null!;
protected override void OnInitialized()
{
try
{
if (UserDataProvider.CurrentRole is not Constants.AdminRole)
{
throw new NotLoggedInException();
}
model.Email = OldEmail;
}
catch (Exception e)
{
ErrorHandler.Handle(e);
}
}
private async Task Save()
{
var request = new ChangeUserAuthDataRequest
{
UserId = Guid.Parse(AuthorityId),
NewAuthData = model
};
var validationResult = await ChangeUserAuthDataRequestValidator.ValidateAsync(request);
if (!validationResult.IsValid)
{
status.SetError(validationResult.ToErrorsString());
return;
}
try
{
status.SetMessage("Wait...");
await Client.ChangeAuthorityAuthDataAsync(request);
status.SetSuccess("Success");
}
catch (Exception e)
{
ErrorHandler.Handle(e);
}
}
}

View File

@@ -0,0 +1,362 @@
@page "/applications/new"
@using System.Net
@using AutoMapper
@using BlazorWebAssemblyVisaApiClient.Validation.VisaApplications.Models
@using BlazorWebAssemblyVisaApiClient.Components.FormComponents.Applicants
@using VisaApiClient
@using BlazorWebAssemblyVisaApiClient.Components
@using BlazorWebAssemblyVisaApiClient.Components.FormComponents.VisaApplications
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
@using BlazorWebAssemblyVisaApiClient.Validation
@using FluentValidation
@using Newtonsoft.Json.Linq
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
<PageTitle>New Application</PageTitle>
<div class="horizontal-centered-content">
<h3>New application</h3>
<EditForm class="form" Model="requestModel" OnValidSubmit="TryCreate">
<ObjectGraphDataAnnotationsValidator/>
<div class="form-block">
<h5>Visa@(Constants.RequiredFieldMarkup)</h5>
<label>
Destination Country:<br/>
<InputText DisplayName="Destination Country" class="rounded" @bind-Value="requestModel.DestinationCountry"/>
</label><br/>
<ValidationMessage For="() => requestModel.DestinationCountry"></ValidationMessage><br/>
<label>
Category:
<EnumInputList Model="requestModel"
EnumProperty="r => r.VisaCategory"
OnChanged="StateHasChanged"/>
</label><br/>
<ValidationMessage For="() => requestModel.VisaCategory"></ValidationMessage><br/>
<label>
Number of entries: <EnumInputList Model="requestModel" EnumProperty="r => r.RequestedNumberOfEntries"/>
</label><br/>
<ValidationMessage For="() => requestModel.RequestedNumberOfEntries"></ValidationMessage><br/>
<label>
For group: <InputCheckbox @bind-Value="requestModel.IsForGroup"/>
</label><br/>
<ValidationMessage For="() => requestModel.IsForGroup"></ValidationMessage><br/>
<label>
Valid for days:<br/>
<InputNumber DisplayName="Valid days" class="rounded" @bind-Value="requestModel.ValidDaysRequested"/>
</label>
<ValidationMessage For="() => requestModel.ValidDaysRequested"></ValidationMessage><br/>
</div>
<div class="form-block">
<h5>Past visas</h5>
@if (requestModel.PastVisas.Count > 0)
{
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Name</th><th>Issue date</th><th>Expiration date</th><th></th>
</tr>
</thead>
<tbody>
@foreach (var visa in requestModel.PastVisas)
{
<tr>
<td>@visa.Name</td>
<td>@visa.IssueDate.ToString("d.MM.yyyy")</td>
<td>@visa.ExpirationDate.ToString("d.MM.yyyy")</td>
<td>
<input type="button" class="border-danger" @onclick="() => RemovePastVisa(visa)" value="X"/>
</td>
</tr>
}
</tbody>
</table>
}
<label>
Name:<br/>
<InputText DisplayName="Past visa name" class="rounded" @bind-Value="editableVisa.Name"/>
</label><br/>
<ValidationMessage For="() => editableVisa.Name"></ValidationMessage><br/>
<label>
Issue date:<br/>
<InputDate DisplayName="Past visa issue date"
class="rounded"
@bind-Value="editableVisa.IssueDate"
max="@formattedNow"/>
</label><br/>
<ValidationMessage For="() => editableVisa.IssueDate"></ValidationMessage><br/>
<label>
Expiration date:<br/>
<InputDate DisplayName="Past visa expiration date"
class="rounded"
@bind-Value="editableVisa.ExpirationDate"/>
</label><br/>
<ValidationMessage For="() => editableVisa.ExpirationDate"></ValidationMessage><br/>
<input type="button" class="btn-outline-primary rounded"
disabled="@(requestModel.PastVisas.Count == ConfigurationConstraints.MaxPastVisas)"
@onclick="AddPastVisa" value="Add"/>
<Status @ref="pastVisaStatus"/>
</div>
<div class="form-block">
<h5>Past visits</h5>
@if (requestModel.PastVisits.Count > 0)
{
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Destination Country</th><th>Start date</th><th>End date</th><th></th>
</tr>
</thead>
<tbody>
@foreach (var visit in requestModel.PastVisits)
{
<tr>
<td>@visit.DestinationCountry</td>
<td>@visit.StartDate.ToString("d.MM.yyyy")</td>
<td>@visit.EndDate.ToString("d.MM.yyyy")</td>
<td>
<input type="button" class="border-danger" @onclick="() => RemovePastVisit(visit)" value="X"/>
</td>
</tr>
}
</tbody>
</table>
}
<label>
Destination Country:<br/>
<InputText DisplayName="Past visit destination Country" class="rounded" @bind-Value="editableVisit.DestinationCountry"/>
</label><br/>
<ValidationMessage For="() => editableVisit.DestinationCountry"></ValidationMessage><br/>
<label>
Start date:<br/>
<InputDate DisplayName="Past visit start date"
class="rounded"
@bind-Value="editableVisit.StartDate"
max="@formattedNow"/>
</label><br/>
<ValidationMessage For="() => editableVisit.StartDate"></ValidationMessage><br/>
<label>
End date:<br/>
<InputDate DisplayName="Past visit end date"
class="rounded"
@bind-Value="editableVisit.EndDate"
max="@formattedNow"/>
</label><br/>
<ValidationMessage For="() => editableVisit.EndDate"></ValidationMessage><br/>
<input type="button" class="btn-outline-primary rounded"
disabled="@(requestModel.PastVisits.Count == ConfigurationConstraints.MaxPastVisits)"
@onclick="AddPastVisit" value="Add"/>
<Status @ref="pastVisitStatus"/>
</div>
@if (requestModel.VisaCategory is VisaCategory.Transit)
{
requestModel.PermissionToDestCountry ??= NewPermissionToDestCountry();
<div class="form-block">
<h5>Permission to destination Country@(Constants.RequiredFieldMarkup)</h5>
<PermissionToDestCountryInput PermissionToDestCountry="requestModel.PermissionToDestCountry"/>
</div>
}
else
{
requestModel.PermissionToDestCountry = null;
}
@if (isNonResident)
{
requestModel.ReentryPermit ??= NewReentryPermit();
<div class="form-block">
<h5>Re-entry permission@(Constants.RequiredFieldMarkup)</h5>
<ReentryPermitInput ReentryPermit="requestModel.ReentryPermit"/>
</div>
}
<br/><input type="submit" class="btn-outline-primary rounded" value="Register"/>
<ValidationSummary/>
<Status @ref="status"/>
</EditForm>
</div>
@code {
//todo past visas and visits
private VisaApplicationCreateRequestModel requestModel = new();
private Status status = null!;
private Status pastVisaStatus = null!;
private Status pastVisitStatus = null!;
private bool isNonResident;
private string formattedNow = null!;
private PastVisaModel editableVisa = null!;
private PastVisitModel editableVisit = null!;
[Inject] IDateTimeProvider DateTimeProvider { get; set; } = null!;
[Inject] IUserDataProvider UserDataProvider { get; set; } = null!;
[Inject] IValidator<VisaApplicationCreateRequestModel> VisaApplicationCreateRequestValidator { get; set; } = null!;
[Inject] IValidator<PastVisaModel> PastVisaModelValidator { get; set; } = null!;
[Inject] IValidator<PastVisitModel> PastVisitModelValidator { get; set; } = null!;
[Inject] IMapper Mapper { get; set; } = null!;
protected override async Task OnInitializedAsync()
{
editableVisa = NewPastVisa();
editableVisit = NewPastVisit();
requestModel.PermissionToDestCountry = NewPermissionToDestCountry();
formattedNow = DateTimeProvider.FormattedNow();
try
{
isNonResident = (await UserDataProvider.GetApplicant()).IsNonResident;
}
catch (Exception e)
{
ErrorHandler.Handle(e);
}
}
private async Task TryCreate()
{
var validationResult = await VisaApplicationCreateRequestValidator.ValidateAsync(requestModel);
if (!validationResult.IsValid)
{
var errorsString = validationResult.ToErrorsString();
status.SetError(errorsString);
}
status.SetMessage("Wait...");
var request = Mapper.Map<VisaApplicationCreateRequest>(requestModel);
try
{
await Client.CreateApplicationAsync(request);
status.SetSuccess("Application created successfully.");
}
catch (ApiException<ProblemDetails> e)
{
if (e.StatusCode == (int)HttpStatusCode.BadRequest
&& e.Result.AdditionalProperties.TryGetValue("errors", out var errors))
{
try
{
var errorsList = ((JArray)errors).ToObject<List<string>>();
status.SetError(string.Join("<br/>", errorsList!));
}
catch (Exception inner)
{
ErrorHandler.Handle(inner);
status.SetError("Error occured");
}
}
else
{
throw;
}
}
catch (Exception e)
{
ErrorHandler.Handle(e);
}
}
private PastVisaModel NewPastVisa()
{
return new()
{
ExpirationDate = DateTimeProvider.Now(),
IssueDate = DateTimeProvider.Now()
};
}
private ReentryPermitModel NewReentryPermit()
{
return new()
{
ExpirationDate = DateTimeProvider.Now()
};
}
private PermissionToDestCountryModel NewPermissionToDestCountry()
{
return new()
{
ExpirationDate = DateTimeProvider.Now()
};
}
private PastVisitModel NewPastVisit()
{
return new()
{
StartDate = DateTimeProvider.Now(),
EndDate = DateTimeProvider.Now()
};
}
private void AddPastVisa()
{
if (requestModel.PastVisas.Count >= ConfigurationConstraints.MaxPastVisas)
{
pastVisaStatus.SetError($"{ConfigurationConstraints.MaxPastVisas} past visas is maximum");
return;
}
var validationResult = PastVisaModelValidator.Validate(editableVisa);
if (!validationResult.IsValid)
{
pastVisaStatus.SetError(validationResult.ToErrorsString());
return;
}
requestModel.PastVisas.Add(editableVisa);
editableVisa = NewPastVisa();
pastVisaStatus.SetSuccess("Added successfully");
}
private void RemovePastVisa(PastVisaModel visa)
{
requestModel.PastVisas.Remove(visa);
}
private void AddPastVisit()
{
if (requestModel.PastVisits.Count >= ConfigurationConstraints.MaxPastVisits)
{
pastVisitStatus.SetError($"{ConfigurationConstraints.MaxPastVisits} past visits is maximum");
return;
}
var validationResult = PastVisitModelValidator.Validate(editableVisit);
if (!validationResult.IsValid)
{
pastVisitStatus.SetError(validationResult.ToErrorsString());
return;
}
requestModel.PastVisits.Add(editableVisit);
editableVisit = NewPastVisit();
pastVisitStatus.SetSuccess("Added successfully");
}
private void RemovePastVisit(PastVisitModel visit)
{
requestModel.PastVisits.Remove(visit);
}
}

View File

@@ -0,0 +1,191 @@
@page "/register"
@using System.Net
@using AutoMapper
@using VisaApiClient
@using BlazorWebAssemblyVisaApiClient.Components.FormComponents.Applicants
@using global::FluentValidation
@using Newtonsoft.Json
@using Newtonsoft.Json.Linq
@using BlazorWebAssemblyVisaApiClient.Components
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
@using BlazorWebAssemblyVisaApiClient.Validation
@using BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
<PageTitle>Registration</PageTitle>
<div class="horizontal-centered-content">
<h3>Registration data</h3>
<EditForm class="form" Model="requestModel" OnValidSubmit="TryRegisterApplicant">
<ObjectGraphDataAnnotationsValidator/>
<div class="form-block">
<h5>Authentication data@(Constants.RequiredFieldMarkup)</h5>
<AuthDataInput AuthData="requestModel.RegisterRequest.AuthData"/>
</div>
<div class="form-block">
<h5>Your Fullname</h5>
<NameInput Name="requestModel.ApplicantName"/>
</div>
<div class="form-block">
<h5>Fullname of your mother</h5>
<NameInput Name="requestModel.MotherName"/>
</div>
<div class="form-block">
<h5>Fullname of your father</h5>
<NameInput Name="requestModel.FatherName"/>
</div>
<div class="form-block">
<h5>Your passport@(Constants.RequiredFieldMarkup)</h5>
<PassportInput Passport="requestModel.Passport"/>
</div>
<div class="form-block">
<h5>Birth data@(Constants.RequiredFieldMarkup)</h5>
<div >
<label>
Country of birth:<br/>
<InputText DisplayName="Country of birth" class="rounded" @bind-Value="requestModel.CountryOfBirth"/>
</label><br/>
<ValidationMessage For="() => requestModel.CountryOfBirth"></ValidationMessage><br/>
<label>
City of birth:<br/>
<InputText DisplayName="City of birth" class="rounded" @bind-Value="requestModel.CityOfBirth"/>
</label><br/>
<ValidationMessage For="() => requestModel.CityOfBirth"></ValidationMessage><br/>
<label>
Birth date:<br/>
<InputDate DisplayName="Birth date" class="rounded" @bind-Value="requestModel.BirthDate" max="@formattedMaxBirthdayDate"/>
</label><br/>
<ValidationMessage For="() => requestModel.BirthDate"></ValidationMessage>
</div>
</div>
<div class="form-block">
<h5>Citizenship@(Constants.RequiredFieldMarkup)</h5>
<div >
<label>
Citizenship:<br/>
<InputText class="rounded" @bind-Value="requestModel.Citizenship"/>
</label><br/>
<ValidationMessage For="() => requestModel.Citizenship"></ValidationMessage><br/>
<label>
Citizenship by birth:<br/>
<InputText DisplayName="Citizenship by birth" class="rounded" @bind-Value="requestModel.CitizenshipByBirth"/>
</label><br/>
<ValidationMessage For="() => requestModel.CitizenshipByBirth"></ValidationMessage>
</div>
</div>
<div class="form-block">
<h5>Address of your place of work@(Constants.RequiredFieldMarkup)</h5>
<div >
<AddressInput Address="requestModel.PlaceOfWork.Address"/>
</div>
</div>
<div class="form-block">
<h5>Place of work data@(Constants.RequiredFieldMarkup)</h5>
<div >
<PlaceOfWorkInput PlaceOfWork="requestModel.PlaceOfWork"/><br/>
<label>
Job title:<br/>
<InputText DisplayName="Job title" class="rounded" @bind-Value="requestModel.JobTitle"/>
</label><br/>
<ValidationMessage For="() => requestModel.JobTitle"></ValidationMessage>
</div>
</div>
<div class="form-block">
<h5>Other</h5>
<div >
<label>
Gender: <EnumInputList Model="requestModel" EnumProperty="r => r.Gender"/>
</label>
</div><br/>
<div >
<label>
Marital status: <EnumInputList Model="requestModel" EnumProperty="r => r.MaritalStatus"/>
</label>
</div><br/>
<div >
<label>
Non-resident: <InputCheckbox @bind-Value="requestModel.IsNonResident"/>
</label>
</div>
</div><br/>
<input type="submit" class="btn-outline-primary" value="Register"/>
<Status @ref="status"/>
</EditForm>
</div>
@code
{
private RegisterApplicantRequestModel requestModel = new();
private Status status = null!;
private string formattedMaxBirthdayDate = null!;
[Inject] IValidator<RegisterApplicantRequestModel> RegisterApplicantRequestValidator { get; set; } = null!;
[Inject] IMapper Mapper { get; set; } = null!;
protected override void OnInitialized()
{
requestModel.BirthDate = DateTime.Now.AddYears(-ConfigurationConstraints.ApplicantMinAge);
formattedMaxBirthdayDate = requestModel.BirthDate.ToString("yyyy-MM-dd");
}
private async void TryRegisterApplicant()
{
var validationResult = await RegisterApplicantRequestValidator.ValidateAsync(requestModel);
if (!validationResult.IsValid)
{
var errorsString = validationResult.ToErrorsString();
status.SetError(errorsString);
return;
}
status.SetMessage("Wait...");
var request = Mapper.Map<RegisterApplicantRequest>(requestModel);
try
{
await Client.RegisterAsync(request);
status.SetSuccess("Register successful. Now log in.");
}
catch (ApiException<ProblemDetails> e)
{
if (e.StatusCode == (int)HttpStatusCode.BadRequest
&& e.Result.AdditionalProperties.TryGetValue("errors", out var errors))
{
var errorsList = ((JArray)errors).ToObject<List<string>>();
if (errorsList is null)
{
ErrorHandler.Handle(new JsonException("Can't convert validation errors to list"));
return;
}
status.SetError(string.Join("<br/>", errorsList));
}
else
{
throw;
}
}
catch (Exception e)
{
status.SetError("Error occured");
ErrorHandler.Handle(e);
}
}
}

View File

@@ -0,0 +1,35 @@
using System.Reflection;
using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider;
using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider;
using FluentValidation;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using VisaApiClient;
namespace BlazorWebAssemblyVisaApiClient;
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
//todo move to launch settings
const string baseAddress = "https://localhost:44370";
//todo make pretty
builder.Services.AddScoped(_ => new HttpClient { BaseAddress = new Uri(baseAddress) });
builder.Services.AddBlazorBootstrap();
builder.Services.AddScoped<Client>(sp => new Client(baseAddress, sp.GetRequiredService<HttpClient>()));
builder.Services.AddSingleton<IDateTimeProvider, DateTimeProvider>();
builder.Services.AddScoped<IUserDataProvider, UserDataProvider>();
builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());
builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
await builder.Build().RunAsync();
}
}

View File

@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:25927",
"sslPort": 44345
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5038",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7200;http://localhost:5038",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using VisaApiClient;
namespace BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models
{
/// Model of place of work with attributes required for validation to work
public class PlaceOfWorkModel
{
[Required]
[StringLength(ConfigurationConstraints.PlaceOfWorkNameLength, MinimumLength = 1)]
public string Name { get; set; } = default!;
[Required]
[ValidateComplexType]
public AddressModel Address { get; set; } = new AddressModel();
[Required]
[StringLength(ConfigurationConstraints.PhoneNumberLength, MinimumLength = ConfigurationConstraints.PhoneNumberMinLength)]
public string PhoneNum { get; set; } = default!;
}
}

View File

@@ -0,0 +1,68 @@
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using VisaApiClient;
namespace BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models
{
/// Model of request with attributes required for validation to work
public class RegisterApplicantRequestModel
{
[Required]
[ValidateComplexType]
public RegisterRequestModel RegisterRequest { get; set; } = new();
[Required]
[ValidateComplexType]
public NameModel ApplicantName { get; set; } = new();
[Required]
[ValidateComplexType]
public PassportModel Passport { get; set; } = new();
[Required(AllowEmptyStrings = true)]
public DateTimeOffset BirthDate { get; set; }
[Required]
[StringLength(70, MinimumLength = 1)]
public string CityOfBirth { get; set; } = default!;
[Required]
[StringLength(70, MinimumLength = 1)]
public string CountryOfBirth { get; set; } = default!;
[Required]
[StringLength(30, MinimumLength = 1)]
public string Citizenship { get; set; } = default!;
[Required]
[StringLength(30, MinimumLength = 1)]
public string CitizenshipByBirth { get; set; } = default!;
[Required(AllowEmptyStrings = true)]
[JsonConverter(typeof(StringEnumConverter))]
public Gender Gender { get; set; }
[Required(AllowEmptyStrings = true)]
[JsonConverter(typeof(StringEnumConverter))]
public MaritalStatus MaritalStatus { get; set; }
[Required]
[ValidateComplexType]
public NameModel FatherName { get; set; } = new();
[Required]
[ValidateComplexType]
public NameModel MotherName { get; set; } = new();
[Required]
[StringLength(50, MinimumLength = 1)]
public string JobTitle { get; set; } = default!;
[Required]
[ValidateComplexType]
public PlaceOfWorkModel PlaceOfWork { get; set; } = new();
public bool IsNonResident { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
using VisaApiClient;
namespace BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models
{
/// Model of request with attributes required for validation to work
public class RegisterRequestModel
{
[Required]
[ValidateComplexType]
public AuthData AuthData { get; set; } = new AuthData();
}
}

View File

@@ -0,0 +1,32 @@
using FluentValidation;
using VisaApiClient;
namespace BlazorWebAssemblyVisaApiClient.Validation.Applicants.Validators;
public class NameModelValidator : AbstractValidator<NameModel>
{
public NameModelValidator()
{
RuleFor(m => m.FirstName)
.NotEmpty()
.WithMessage("First Name can not be empty")
.Matches(Constants.EnglishWordRegex)
.WithMessage("First name must be in english characters")
.MaximumLength(ConfigurationConstraints.NameLength)
.WithMessage($"First Name length must be less than {ConfigurationConstraints.NameLength}");
RuleFor(m => m.Surname)
.NotEmpty()
.WithMessage("Surname can not be empty")
.Matches(Constants.EnglishWordRegex)
.WithMessage("Surname must be in english characters")
.MaximumLength(ConfigurationConstraints.NameLength)
.WithMessage($"Surname length must be less than {ConfigurationConstraints.NameLength}");
RuleFor(m => m.Patronymic)
.Matches(Constants.EnglishWordRegex)
.WithMessage("Patronymic must be in english characters")
.MaximumLength(ConfigurationConstraints.NameLength)
.WithMessage($"Patronymic length must be less than {ConfigurationConstraints.NameLength}");
}
}

View File

@@ -0,0 +1,39 @@
using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider;
using FluentValidation;
using VisaApiClient;
namespace BlazorWebAssemblyVisaApiClient.Validation.Applicants.Validators;
public class PassportModelValidator : AbstractValidator<PassportModel>
{
public PassportModelValidator(IDateTimeProvider dateTimeProvider)
{
RuleFor(r => r.Issuer)
.NotEmpty()
.WithMessage("Passport issuer can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Passport issuer field can contain only english letters, digits and special symbols")
.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")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Passport number field can contain only english letters, digits and special symbols")
.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");
}
}

Some files were not shown because too many files have changed in this diff Show More